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

Issue:
Conformance: Remove Unused Namespace
The following namespace exists in `conformance/resources/manifests/manifests.yaml` and is unused:

```yaml
# Namespace for simple web server backends. This is expected by
# the upstream conformance suite's Setup method.
apiVersion: v1
kind: Namespace
metadata:
  name: gateway-conformance-web-backend
  labels:
    gateway-conformance: web-backend
```

If needed, explore upstream (Gateway API) options for making this namespace optional.



Discussion:
@danehans: cc: @SinaChavoshi @zetxqx: Hi @danehans , thanks for noticing it.

I found it too. And proposed a refactor(cleanup and moving common resources to shared place) PR #982 to remove it. 
Would you like to take a look?@zetxqx: I believe this can be closed since #982 is merged.@robscott: > I believe this can be closed since [#982](https://github.com/kubernetes-sigs/gateway-api-inference-extension/pull/982) is merged.

Thanks for the reminder @zetxqx!

/close
@k8s-ci-robot: @robscott: Closing this issue.

<details>

In response to [this](https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/990#issuecomment-2993096568):

>> I believe this can be closed since [#982](https://github.com/kubernetes-sigs/gateway-api-inference-extension/pull/982) is merged.
>
>Thanks for the reminder @zetxqx!
>
>/close
>


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

Solution patch:
diff --git a/PROJECT b/PROJECT
index 75c9c9c..c049fc8 100644
--- a/PROJECT
+++ b/PROJECT
@@ -24,4 +24,12 @@ resources:
   kind: InferenceModel
   path: sigs.k8s.io/gateway-api-inference-extension/api/v1alpha1
   version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  domain: x-k8s.io
+  group: inference
+  kind: EndpointPickerConfig
+  path: sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1
+  version: v1alpha1
 version: "3"
diff --git a/api/config/v1alpha1/defaults.go b/api/config/v1alpha1/defaults.go
new file mode 100644
index 0000000..f59bc7c
--- /dev/null
+++ b/api/config/v1alpha1/defaults.go
@@ -0,0 +1,29 @@
+/*
+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 v1alpha1
+
+// SetDefaults_EndpointPickerConfig sets default values in a
+// EndpointPickerConfig struct.
+//
+// This naming convension is required by the defalter-gen code.
+func SetDefaults_EndpointPickerConfig(cfg *EndpointPickerConfig) {
+	for idx, pluginConfig := range cfg.Plugins {
+		if pluginConfig.Name == "" {
+			cfg.Plugins[idx].Name = pluginConfig.PluginName
+		}
+	}
+}
diff --git a/api/config/v1alpha1/doc.go b/api/config/v1alpha1/doc.go
new file mode 100644
index 0000000..122c3b9
--- /dev/null
+++ b/api/config/v1alpha1/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 v1alpha1 contains API Schema definitions for the
+// inference.networking.x-k8s.io API group.
+//
+// +kubebuilder:object:generate=true
+// +groupName=inference.networking.x-k8s.io
+package v1alpha1
diff --git a/api/config/v1alpha1/endpointpickerconfig_types.go b/api/config/v1alpha1/endpointpickerconfig_types.go
new file mode 100644
index 0000000..86f5ab7
--- /dev/null
+++ b/api/config/v1alpha1/endpointpickerconfig_types.go
@@ -0,0 +1,92 @@
+/*
+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 v1alpha1
+
+import (
+	"encoding/json"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +k8s:defaulter-gen=true
+// +kubebuilder:object:root=true
+
+// EndpointPickerConfig is the Schema for the endpointpickerconfigs API
+type EndpointPickerConfig struct {
+	metav1.TypeMeta `json:",inline"`
+
+	// +required
+	// +kubebuilder:validation:Required
+	// Plugins is the list of plugins that will be instantiated.
+	Plugins []PluginSpec `json:"plugins"`
+
+	// +required
+	// +kubebuilder:validation:Required
+	// SchedulingProfiles is the list of named SchedulingProfiles
+	// that will be created.
+	SchedulingProfiles []SchedulingProfile `json:"schedulingProfiles"`
+}
+
+// PluginSpec contains the information that describes a plugin that
+// will be instantiated.
+type PluginSpec struct {
+	// +optional
+	// Name provides a name for plugin entries to reference. If
+	// omitted, the value of the PluginName field will be used.
+	Name string `json:"name"`
+
+	// +required
+	// +kubebuilder:validation:Required
+	// PluginName specifies the plugin to be instantiated.
+	PluginName string `json:"pluginName"`
+
+	// +optional
+	// Parameters are the set of parameters to be passed to the plugin's
+	// factory function. The factory function is responsible
+	// to parse the parameters.
+	Parameters json.RawMessage `json:"parameters"`
+}
+
+// SchedulingProfile contains the information to create a SchedulingProfile
+// entry to be used by the scheduler.
+type SchedulingProfile struct {
+	// +kubebuilder:validation:Required
+	// Name specifies the name of this SchedulingProfile
+	Name string `json:"name"`
+
+	// +required
+	// +kubebuilder:validation:Required
+	// Plugins is the list of plugins for this SchedulingProfile. They are assigned
+	// to the appropriate "slots" based on their type.
+	Plugins []SchedulingPlugin `json:"plugins"`
+}
+
+// SchedulingPlugin describes a plugin that will be associated with a
+// SchedulingProfile entry.
+type SchedulingPlugin struct {
+	// +required
+	// +kubebuilder:validation:Required
+	// PluginRef specifies a partiular Plugin instance to be associated with
+	// this SchedulingProfile. The reference is to the name of an
+	// entry of the Plugins defined in the configuration's Plugins
+	// section
+	PluginRef string `json:"pluginRef"`
+
+	// +optional
+	// Weight is the weight fo be used if this plugin is a Scorer.
+	Weight *int `json:"weight"`
+}
diff --git a/api/config/v1alpha1/zz_generated.deepcopy.go b/api/config/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 0000000..1326f35
--- /dev/null
+++ b/api/config/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,126 @@
+//go:build !ignore_autogenerated
+
+/*
+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 controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"encoding/json"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *EndpointPickerConfig) DeepCopyInto(out *EndpointPickerConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	if in.Plugins != nil {
+		in, out := &in.Plugins, &out.Plugins
+		*out = make([]PluginSpec, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.SchedulingProfiles != nil {
+		in, out := &in.SchedulingProfiles, &out.SchedulingProfiles
+		*out = make([]SchedulingProfile, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointPickerConfig.
+func (in *EndpointPickerConfig) DeepCopy() *EndpointPickerConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(EndpointPickerConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *EndpointPickerConfig) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PluginSpec) DeepCopyInto(out *PluginSpec) {
+	*out = *in
+	if in.Parameters != nil {
+		in, out := &in.Parameters, &out.Parameters
+		*out = make(json.RawMessage, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginSpec.
+func (in *PluginSpec) DeepCopy() *PluginSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PluginSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SchedulingPlugin) DeepCopyInto(out *SchedulingPlugin) {
+	*out = *in
+	if in.Weight != nil {
+		in, out := &in.Weight, &out.Weight
+		*out = new(int)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchedulingPlugin.
+func (in *SchedulingPlugin) DeepCopy() *SchedulingPlugin {
+	if in == nil {
+		return nil
+	}
+	out := new(SchedulingPlugin)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SchedulingProfile) DeepCopyInto(out *SchedulingProfile) {
+	*out = *in
+	if in.Plugins != nil {
+		in, out := &in.Plugins, &out.Plugins
+		*out = make([]SchedulingPlugin, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchedulingProfile.
+func (in *SchedulingProfile) DeepCopy() *SchedulingProfile {
+	if in == nil {
+		return nil
+	}
+	out := new(SchedulingProfile)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/api/config/v1alpha1/zz_generated.defaults.go b/api/config/v1alpha1/zz_generated.defaults.go
new file mode 100644
index 0000000..1cf0c42
--- /dev/null
+++ b/api/config/v1alpha1/zz_generated.defaults.go
@@ -0,0 +1,38 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+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 defaulter-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// RegisterDefaults adds defaulters functions to the given scheme.
+// Public to allow building arbitrary schemes.
+// All generated defaulters are covering - they call all nested defaulters.
+func RegisterDefaults(scheme *runtime.Scheme) error {
+	scheme.AddTypeDefaultingFunc(&EndpointPickerConfig{}, func(obj interface{}) { SetObjectDefaults_EndpointPickerConfig(obj.(*EndpointPickerConfig)) })
+	return nil
+}
+
+func SetObjectDefaults_EndpointPickerConfig(in *EndpointPickerConfig) {
+	SetDefaults_EndpointPickerConfig(in)
+}
diff --git a/api/config/v1alpha1/zz_generated.register.go b/api/config/v1alpha1/zz_generated.register.go
new file mode 100644
index 0000000..3c48d0c
--- /dev/null
+++ b/api/config/v1alpha1/zz_generated.register.go
@@ -0,0 +1,69 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+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 register-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// GroupName specifies the group name used to register the objects.
+const GroupName = "inference.networking.x-k8s.io"
+
+// GroupVersion specifies the group and the version used to register the objects.
+var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1alpha1"}
+
+// SchemeGroupVersion is group version used to register these objects
+// Deprecated: use GroupVersion instead.
+var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
+
+// Resource takes an unqualified resource and returns a Group qualified GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+var (
+	// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
+	SchemeBuilder      runtime.SchemeBuilder
+	localSchemeBuilder = &SchemeBuilder
+	// Deprecated: use Install instead
+	AddToScheme = localSchemeBuilder.AddToScheme
+	Install     = localSchemeBuilder.AddToScheme
+)
+
+func init() {
+	// We only register manually written functions here. The registration of the
+	// generated functions takes place in the generated files. The separation
+	// makes the code compile even when the generated files are missing.
+	localSchemeBuilder.Register(addKnownTypes)
+}
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&EndpointPickerConfig{},
+	)
+	// AddToGroupVersion allows the serialization of client types like ListOptions.
+	v1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/cmd/epp/main.go b/cmd/epp/main.go
index 609a1e5..5d5b2c5 100644
--- a/cmd/epp/main.go
+++ b/cmd/epp/main.go
@@ -25,6 +25,9 @@ import (
 )
 
 func main() {
+	// Register all known plugin factories
+	runner.RegisterAllPlgugins()
+
 	if err := runner.NewRunner().Run(ctrl.SetupSignalHandler()); err != nil {
 		os.Exit(1)
 	}
diff --git a/cmd/epp/runner/register.go b/cmd/epp/runner/register.go
new file mode 100644
index 0000000..ab525a2
--- /dev/null
+++ b/cmd/epp/runner/register.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 runner
+
+import (
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+	"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/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"
+)
+
+// RegisterAllPlgugins registers the factory functions of all known plugins
+func RegisterAllPlgugins() {
+	plugins.Register(filter.LeastKVCacheFilterName, filter.LeastKVCacheFilterFactory)
+	plugins.Register(filter.LeastQueueFilterName, filter.LeastQueueFilterFactory)
+	plugins.Register(filter.LoraAffinityFilterName, filter.LoraAffinityFilterFactory)
+	plugins.Register(filter.LowQueueFilterName, filter.LowQueueFilterFactory)
+	plugins.Register(prefix.PrefixCachePluginName, prefix.PrefixCachePluginFactory)
+	plugins.Register(picker.MaxScorePickerName, picker.MaxScorePickerFactory)
+	plugins.Register(picker.RandomPickerName, picker.RandomPickerFactory)
+	plugins.Register(profile.SingleProfileHandlerName, profile.SingleProfileHandlerFactory)
+	plugins.Register(scorer.KvCacheScorerName, scorer.KvCacheScorerFactory)
+	plugins.Register(scorer.QueueScorerName, scorer.QueueScorerFactory)
+}
+
+// eppHandle is a temporary implementation of the interface plugins.Handle
+type eppHandle struct {
+}
diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go
index 194ad9d..ea4fba0 100644
--- a/cmd/epp/runner/runner.go
+++ b/cmd/epp/runner/runner.go
@@ -35,12 +35,15 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 
+	"sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
 	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"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics/collectors"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"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"
@@ -109,6 +112,9 @@ var (
 	loraInfoMetric = flag.String("loraInfoMetric",
 		"vllm:lora_requests_info",
 		"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")
 
 	setupLog = ctrl.Log.WithName("setup")
 
@@ -211,6 +217,30 @@ func (r *Runner) Run(ctx context.Context) error {
 		return err
 	}
 
+	var theConfig *v1alpha1.EndpointPickerConfig
+	var instantiatedPlugins map[string]plugins.Plugin
+
+	if len(*configText) != 0 || len(*configFile) != 0 {
+		theConfig, err = config.LoadConfig([]byte(*configText), *configFile)
+		if err != nil {
+			setupLog.Error(err, "Failed to load the configuration")
+			return err
+		}
+
+		epp := eppHandle{}
+		instantiatedPlugins, err = config.LoadPluginReferences(theConfig.Plugins, epp)
+		if err != nil {
+			setupLog.Error(err, "Failed to instantiate the plugins")
+			return err
+		}
+
+		r.schedulerConfig, err = scheduling.LoadSchedulerConfig(theConfig.SchedulingProfiles, instantiatedPlugins, setupLog)
+		if err != nil {
+			setupLog.Error(err, "Failed to create Scheduler configuration")
+			return err
+		}
+	}
+
 	// --- Initialize Core EPP Components ---
 	scheduler, err := r.initializeScheduler(datastore)
 	if err != nil {
@@ -220,6 +250,10 @@ func (r *Runner) Run(ctx context.Context) error {
 
 	saturationDetector := saturationdetector.NewDetector(sdConfig, datastore, ctrl.Log)
 
+	// Add requestControl plugins
+	if instantiatedPlugins != nil {
+		r.requestControlConfig.AddPlugins(instantiatedPlugins)
+	}
 	director := requestcontrol.NewDirectorWithConfig(datastore, scheduler, saturationDetector, r.requestControlConfig)
 
 	// --- Setup ExtProc Server Runner ---
@@ -354,6 +388,9 @@ 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")
+	}
 
 	return nil
 }
diff --git a/config/charts/body-based-routing/templates/istio.yaml b/config/charts/body-based-routing/templates/istio.yaml
index 6d4535c..2c744e2 100644
--- a/config/charts/body-based-routing/templates/istio.yaml
+++ b/config/charts/body-based-routing/templates/istio.yaml
@@ -31,7 +31,7 @@ spec:
             response_trailer_mode: "SKIP"
           grpc_service:
             envoy_grpc:
-              cluster_name: outbound|{{ .Values.bbr.port }}||{{ .Values.bbr.name }}.default.svc.cluster.local
+              cluster_name: outbound|{{ .Values.bbr.port }}||{{ .Values.bbr.name }}.{{ .Release.Namespace }}.svc.cluster.local
 ---
 apiVersion: networking.istio.io/v1
 kind: DestinationRule
@@ -39,7 +39,7 @@ metadata:
   name: {{ .Values.bbr.name }}
   namespace: {{ .Release.Namespace }}
 spec:
-  host: {{ .Values.bbr.name }}.default.svc.cluster.local
+  host: {{ .Values.bbr.name }}.{{ .Release.Namespace }}.svc.cluster.local
   trafficPolicy:
       tls:
         mode: SIMPLE
diff --git a/conformance/conformance.go b/conformance/conformance.go
index 66b8c09..952ff96 100644
--- a/conformance/conformance.go
+++ b/conformance/conformance.go
@@ -68,8 +68,8 @@ import (
 
 // Constants for the shared Gateway
 const (
-	SharedGatewayName      = "conformance-gateway"       // Name of the Gateway in manifests.yaml
-	SharedGatewayNamespace = "gateway-conformance-infra" // Namespace of the Gateway
+	SharedGatewayName      = "conformance-primary-gateway" // Name of the Gateway in manifests.yaml
+	SharedGatewayNamespace = "gateway-conformance-infra"   // Namespace of the Gateway
 )
 
 // GatewayLayerProfileName defines the name for the conformance profile that tests
@@ -88,6 +88,7 @@ const SupportInferencePool features.FeatureName = "SupportInferencePool"
 // of the "Gateway" profile for the Inference Extension MUST support.
 var InferenceCoreFeatures = sets.New(
 	features.SupportGateway, // This is needed to ensure manifest gets applied during setup.
+	features.SupportHTTPRoute,
 	SupportInferencePool,
 )
 
diff --git a/conformance/reports/README.md b/conformance/reports/README.md
index 81652b1..ba0278d 100644
--- a/conformance/reports/README.md
+++ b/conformance/reports/README.md
@@ -6,6 +6,7 @@ This directory stores conformance reports submitted by various implementations o
 
 This folder stores conformance reports organized first by the version of the Gateway API Inference Extension specification they were tested against, and then by the specific conformance profile (e.g., Gateway, EPP, Model Server):
 
+```
 |-- conformance/reports
 |   |-- v0.3.0  # Example extension version
 |   |   |-- gateway  # Conformance profile/category
@@ -22,6 +23,7 @@ This folder stores conformance reports organized first by the version of the Gat
 |   |   |   |-- ...
 |   |-- v0.4.0 # Future extension version
 |   |   |-- ...
+```
 
 ## Implementation Submissions
 
diff --git a/conformance/resources/manifests/manifests.yaml b/conformance/resources/manifests/manifests.yaml
index a836ece..c4f9b1f 100644
--- a/conformance/resources/manifests/manifests.yaml
+++ b/conformance/resources/manifests/manifests.yaml
@@ -18,17 +18,12 @@ metadata:
   labels:
     gateway-conformance: backend
 ---
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: gateway-conformance-web-backend
-  labels:
-    gateway-conformance: web-backend
----
+# A basic Gateway resource that allows HTTPRoutes from the same namespace.
+# Tests can use this as a parent reference for routes that target InferencePools.
 apiVersion: gateway.networking.k8s.io/v1
 kind: Gateway
 metadata:
-  name: conformance-gateway
+  name: conformance-primary-gateway
   namespace: gateway-conformance-infra
 spec:
   gatewayClassName: "{GATEWAY_CLASS_NAME}"
@@ -58,30 +53,32 @@ spec:
     allowedRoutes:
       namespaces:
         from: All
+
+### The following defines the essential resources for the gateway conformance test.
+### All resources are created in the 'gateway-conformance-app-backend' namespace.
 ---
+# Deploys a mock backend service to act as a model server.
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: inference-model-1
+  name: primary-inference-model-server-deployment
   namespace: gateway-conformance-app-backend
   labels:
-    app: inference-model-1
+    app: primary-inference-model-server
 spec:
-  replicas: 1
   selector:
     matchLabels:
-      app: inference-model-1
+      app: primary-inference-model-server
   template:
     metadata:
       labels:
-        app: inference-model-1
+        app: primary-inference-model-server
     spec:
       containers:
-      - name: echo-basic-1
+      - name: echoserver
         image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
         ports:
-        - name: http
-          containerPort: 3000
+        - containerPort: 3000
         readinessProbe:
           httpGet:
             path: /
@@ -103,29 +100,28 @@ spec:
             fieldRef:
               fieldPath: status.podIP
 ---
+# Deploys a secondary mock backend service to act as a model server.
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: inference-model-2
+  name: secondary-inference-model-server-deployment
   namespace: gateway-conformance-app-backend
   labels:
-    app: inference-model-2
+    app: secondary-inference-model-server
 spec:
-  replicas: 1
   selector:
     matchLabels:
-      app: inference-model-2
+      app: secondary-inference-model-server
   template:
     metadata:
       labels:
-        app: inference-model-2
+        app: secondary-inference-model-server
     spec:
       containers:
-      - name: echo-basic-2
+      - name: echoserver
         image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
         ports:
-        - name: http
-          containerPort: 3000
+        - containerPort: 3000
         readinessProbe:
           httpGet:
             path: /
@@ -146,3 +142,209 @@ spec:
           valueFrom:
             fieldRef:
               fieldPath: status.podIP
+---
+# --- Primary InferencePool Definition ---
+apiVersion: inference.networking.x-k8s.io/v1alpha2
+kind: InferencePool
+metadata:
+  name: primary-inference-pool
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: primary-inference-model-server
+  targetPortNumber: 3000
+  extensionRef:
+    name: primary-endpoint-picker-svc
+---
+# --- Primary Conformance EPP service Definition ---
+apiVersion: v1
+kind: Service
+metadata:
+  name: primary-endpoint-picker-svc
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: primary-app-backend-epp
+  ports:
+    - protocol: TCP
+      port: 9002
+      targetPort: 9002
+      appProtocol: http2
+  type: ClusterIP
+---
+# --- Primary Conformance EPP Deployment ---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: primary-app-endpoint-picker
+  namespace: gateway-conformance-app-backend
+  labels:
+    app: primary-app-backend-epp
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: primary-app-backend-epp
+  template:
+    metadata:
+      labels:
+        app: primary-app-backend-epp
+    spec:
+      # Conservatively, this timeout should mirror the longest grace period of the pods within the pool
+      terminationGracePeriodSeconds: 130
+      containers:
+      - name: epp
+        image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
+        imagePullPolicy: Always
+        args:
+        - -poolName
+        - "primary-inference-pool"
+        - -poolNamespace
+        - "gateway-conformance-app-backend"
+        - -v
+        - "4"
+        - --zap-encoder
+        - "json"
+        - -grpcPort
+        - "9002"
+        - -grpcHealthPort
+        - "9003"
+        env:
+        - name: USE_STREAMING
+          value: "true"
+        - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING # Used for conformance test.
+          value: "true"
+        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
+---
+# --- Secondary InferencePool Definition ---
+apiVersion: inference.networking.x-k8s.io/v1alpha2
+kind: InferencePool
+metadata:
+  name: secondary-inference-pool
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: secondary-inference-model-server
+  targetPortNumber: 3000
+  extensionRef:
+    name: secondary-endpoint-picker-svc
+---
+# --- Secondary Conformance EPP service Definition ---
+apiVersion: v1
+kind: Service
+metadata:
+  name: secondary-endpoint-picker-svc
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: secondary-app-backend-epp
+  ports:
+    - protocol: TCP
+      port: 9002
+      targetPort: 9002
+      appProtocol: http2
+  type: ClusterIP
+---
+# --- Secondary Conformance EPP Deployment ---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: secondary-app-endpoint-picker
+  namespace: gateway-conformance-app-backend
+  labels:
+    app: secondary-app-backend-epp
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: secondary-app-backend-epp
+  template:
+    metadata:
+      labels:
+        app: secondary-app-backend-epp
+    spec:
+      # Conservatively, this timeout should mirror the longest grace period of the pods within the pool
+      terminationGracePeriodSeconds: 130
+      containers:
+      - name: epp
+        image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
+        imagePullPolicy: Always
+        args:
+        - -poolName
+        - "secondary-inference-pool"
+        - -poolNamespace
+        - "gateway-conformance-app-backend"
+        - -v
+        - "4"
+        - --zap-encoder
+        - "json"
+        - -grpcPort
+        - "9002"
+        - -grpcHealthPort
+        - "9003"
+        env:
+        - name: USE_STREAMING
+          value: "true"
+        - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING # Used for conformance test.
+          value: "true"
+        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
+---
+# --- Required Role and RoleBinding for Conformance Test for EPP ---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: inference-model-reader
+  namespace: gateway-conformance-app-backend
+rules:
+- apiGroups: ["inference.networking.x-k8s.io"]
+  resources: ["inferencemodels", "inferencepools"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["pods"]
+  verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: epp-to-inference-model-reader
+  namespace: gateway-conformance-app-backend
+subjects:
+- kind: ServiceAccount
+  name: default
+  namespace: gateway-conformance-app-backend
+roleRef:
+  kind: Role
+  name: inference-model-reader
+  apiGroup: rbac.authorization.k8s.io
diff --git a/mkdocs.yml b/mkdocs.yml
index cc76400..19a54e4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -71,6 +71,9 @@ nav:
       - Configuration Guide:
           - Prefix Cache Aware Plugin: guides/epp-configuration/prefix-aware.md
     - Implementer's Guide: guides/implementers.md
+    - Implementer Guides:
+      - Getting started: guides/implementers.md
+      - Conformance Tests: guides/conformance-tests.md
   - Performance:
     - Benchmark: performance/benchmark/index.md
     - Regression Testing: performance/regression-testing/index.md
diff --git a/pkg/epp/common/config/configloader.go b/pkg/epp/common/config/configloader.go
new file mode 100644
index 0000000..7db82a0
--- /dev/null
+++ b/pkg/epp/common/config/configloader.go
@@ -0,0 +1,144 @@
+/*
+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 config
+
+import (
+	"errors"
+	"fmt"
+	"os"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+
+	configapi "sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+)
+
+var scheme = runtime.NewScheme()
+
+func init() {
+	configapi.SchemeBuilder.Register(configapi.RegisterDefaults)
+	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{}
+
+	codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict)
+	err = runtime.DecodeInto(codecs.UniversalDecoder(), configText, theConfig)
+	if err != nil {
+		return nil, fmt.Errorf("the configuration is invalid. Error: %s", err)
+	}
+
+	// Validate loaded configuration
+	err = validateConfiguration(theConfig)
+	if err != nil {
+		return nil, fmt.Errorf("the configuration is invalid. error: %s", err)
+	}
+	return theConfig, nil
+}
+
+func LoadPluginReferences(thePlugins []configapi.PluginSpec, handle plugins.Handle) (map[string]plugins.Plugin, error) {
+	references := map[string]plugins.Plugin{}
+	for _, pluginConfig := range thePlugins {
+		thePlugin, err := InstantiatePlugin(pluginConfig, handle)
+		if err != nil {
+			return nil, err
+		}
+		references[pluginConfig.Name] = thePlugin
+	}
+	return references, nil
+}
+
+func InstantiatePlugin(pluginSpec configapi.PluginSpec, handle plugins.Handle) (plugins.Plugin, error) {
+	factory, ok := plugins.Registry[pluginSpec.PluginName]
+	if !ok {
+		return nil, fmt.Errorf("failed to instantiate the plugin. plugin %s not found", pluginSpec.PluginName)
+	}
+	thePlugin, err := factory(pluginSpec.Name, pluginSpec.Parameters, handle)
+	if err != nil {
+		return nil, fmt.Errorf("failed to instantiate the plugin %s. Error: %s", pluginSpec.PluginName, err)
+	}
+	return thePlugin, err
+}
+
+func validateConfiguration(theConfig *configapi.EndpointPickerConfig) error {
+	names := make(map[string]struct{})
+
+	for _, pluginConfig := range theConfig.Plugins {
+		if pluginConfig.PluginName == "" {
+			return errors.New("plugin reference definition missing a plugin name")
+		}
+
+		if _, ok := names[pluginConfig.Name]; ok {
+			return fmt.Errorf("the name %s has been specified for more than one plugin", pluginConfig.Name)
+		}
+		names[pluginConfig.Name] = struct{}{}
+
+		_, ok := plugins.Registry[pluginConfig.PluginName]
+		if !ok {
+			return fmt.Errorf("plugin %s is not found", pluginConfig.PluginName)
+		}
+	}
+
+	if len(theConfig.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 {
+		if profile.Name == "" {
+			return errors.New("SchedulingProfiles need a name")
+		}
+
+		if _, ok := names[profile.Name]; ok {
+			return fmt.Errorf("the name %s has been specified for more than one SchedulingProfile", profile.Name)
+		}
+		names[profile.Name] = struct{}{}
+
+		if len(profile.Plugins) == 0 {
+			return errors.New("SchedulingProfiles need at least one plugin")
+		}
+		for _, plugin := range profile.Plugins {
+			if len(plugin.PluginRef) == 0 {
+				return errors.New("SchedulingProfile's plugins need a plugin reference")
+			}
+
+			notFound := true
+			for _, pluginConfig := range theConfig.Plugins {
+				if plugin.PluginRef == pluginConfig.Name {
+					notFound = false
+					break
+				}
+			}
+			if notFound {
+				return errors.New(plugin.PluginRef + " is a reference to an undefined Plugin")
+			}
+		}
+	}
+	return nil
+}
diff --git a/pkg/epp/plugins/plugins.go b/pkg/epp/plugins/plugins.go
index 5dd8d87..fd0f64c 100644
--- a/pkg/epp/plugins/plugins.go
+++ b/pkg/epp/plugins/plugins.go
@@ -22,3 +22,7 @@ type Plugin interface {
 	// Name returns the name of the plugin.
 	Name() string
 }
+
+// Handle provides plugins  set of standard data and tools to work with
+type Handle interface {
+}
diff --git a/pkg/epp/plugins/registry.go b/pkg/epp/plugins/registry.go
new file mode 100644
index 0000000..d33ce99
--- /dev/null
+++ b/pkg/epp/plugins/registry.go
@@ -0,0 +1,33 @@
+/*
+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 (
+	"encoding/json"
+)
+
+// Factory is the definition of the factory functions that are used to instantiate plugins
+// specified in a configuration.
+type Factory func(name string, parameters json.RawMessage, handle Handle) (Plugin, error)
+
+// Register is a static function that can be called to register plugin factory functions.
+func Register(name string, factory Factory) {
+	Registry[name] = factory
+}
+
+// Registry is a mapping from plugin name to Factory function
+var Registry map[string]Factory = map[string]Factory{}
diff --git a/pkg/epp/registry/registry.go b/pkg/epp/registry/registry.go
new file mode 100644
index 0000000..8ea49da
--- /dev/null
+++ b/pkg/epp/registry/registry.go
@@ -0,0 +1,35 @@
+/*
+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 registry
+
+import (
+	"encoding/json"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+)
+
+// Factory is the definition of the factory functions that are used to instantiate plugins
+// specified in a configuration.
+type Factory func(parameters json.RawMessage) (plugins.Plugin, error)
+
+// Register is a static function that can be called to register plugin factory functions.
+func Register(name string, factory Factory) {
+	Registry[name] = factory
+}
+
+// Registry is a mapping from plugin name to Factory function
+var Registry map[string]Factory = map[string]Factory{}
diff --git a/pkg/epp/requestcontrol/director.go b/pkg/epp/requestcontrol/director.go
index 38002c9..74e3c88 100644
--- a/pkg/epp/requestcontrol/director.go
+++ b/pkg/epp/requestcontrol/director.go
@@ -22,6 +22,7 @@ import (
 	"context"
 	"fmt"
 	"math/rand"
+	"net"
 	"strconv"
 	"time"
 
@@ -54,6 +55,7 @@ func NewDirectorWithConfig(datastore datastore.Datastore, scheduler Scheduler, s
 		datastore:           datastore,
 		scheduler:           scheduler,
 		saturationDetector:  saturationDetector,
+		preRequestPlugins:   config.preRequestPlugins,
 		postResponsePlugins: config.postResponsePlugins,
 	}
 }
@@ -63,14 +65,15 @@ type Director struct {
 	datastore           datastore.Datastore
 	scheduler           Scheduler
 	saturationDetector  SaturationDetector
+	preRequestPlugins   []PreRequest
 	postResponsePlugins []PostResponse
 }
 
 // HandleRequest orchestrates the request lifecycle:
 //  1. Parses request details.
-//  2. Calls PreDispatch for admission control.
-//  3. Calls Dispatch (which calls Scheduler) if request is approved.
-//  4. Calls PostDispatch to populate RequestContext with results.
+//  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.
 //
 // 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) {
@@ -117,42 +120,39 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 		Prompt:      prompt,
 		Headers:     reqCtx.Request.Headers,
 	}
-	logger = logger.WithValues(
-		"model", reqCtx.Model,
-		"resolvedTargetModel", reqCtx.ResolvedTargetModel,
-		"criticality", requestCriticality,
-	)
+
+	logger = logger.WithValues("model", reqCtx.Model, "resolvedTargetModel", reqCtx.ResolvedTargetModel, "criticality", requestCriticality)
 	ctx = log.IntoContext(ctx, logger)
 	logger.V(logutil.DEBUG).Info("LLM request assembled")
 
-	// --- 2. Saturation Check ---
-	preDispatchErr := d.PreDispatch(ctx, reqCtx, requestCriticality)
-	if preDispatchErr != nil {
-		return reqCtx, preDispatchErr
+	// --- 2. Admission Control check --
+	if err := d.admitRequest(ctx, requestCriticality); err != nil {
+		return reqCtx, err
 	}
 
-	// --- 3. Dispatch (Calls Scheduler) ---
-	results, dispatchErr := d.Dispatch(ctx, reqCtx.SchedulingRequest)
-	if dispatchErr != nil {
-		return reqCtx, dispatchErr
+	// --- 3. Call Scheduler ---
+	results, err := d.scheduler.Schedule(ctx, reqCtx.SchedulingRequest)
+	if err != nil {
+		return reqCtx, errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: fmt.Errorf("failed to find target pod: %w", err).Error()}
 	}
 
-	// --- 4. PostDispatch (Populates RequestContext) ---
-	// Insert target endpoint to instruct Envoy to route requests to the specified target pod.
-	// Attach the port number.
-	reqCtx, postDispatchErr := d.PostDispatch(ctx, reqCtx, results)
-	if postDispatchErr != nil {
-		return reqCtx, postDispatchErr
+	// --- 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)
+	if err != nil {
+		return reqCtx, err
 	}
 
 	return reqCtx, nil
 }
 
-// PreDispatch handles admission control before dispatch.
-func (d *Director) PreDispatch(ctx context.Context, reqCtx *handlers.RequestContext, reqCriticality v1alpha2.Criticality) error {
+// admitRequest handles admission control to decide whether or not to accept the request
+// based on the request criticality and system saturation state.
+func (d *Director) admitRequest(ctx context.Context, requestCriticality v1alpha2.Criticality) error {
 	logger := log.FromContext(ctx)
 
-	if reqCriticality == v1alpha2.Critical {
+	if requestCriticality == v1alpha2.Critical {
 		logger.V(logutil.DEBUG).Info("Critical request bypassing saturation check.")
 		return nil
 	}
@@ -164,24 +164,14 @@ func (d *Director) PreDispatch(ctx context.Context, reqCtx *handlers.RequestCont
 			Msg:  "system saturated, non-critical request dropped",
 		}
 	}
-	return nil
-}
-
-// Dispatch runs one or many scheduling cycles.
-func (d *Director) Dispatch(ctx context.Context, llmReq *schedulingtypes.LLMRequest) (*schedulingtypes.SchedulingResult, error) {
-	var err error
-	res, err := d.scheduler.Schedule(ctx, llmReq)
-	if err != nil {
-		return nil, errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: fmt.Errorf("failed to find target pod: %w", err).Error()}
-	}
 
-	return res, nil // TODO handle multi cycle result after defining the PostDispatch extension point
+	return nil
 }
 
-// PostDispatch populates the RequestContext based on scheduling results.
-func (d *Director) PostDispatch(ctx context.Context, reqCtx *handlers.RequestContext, result *schedulingtypes.SchedulingResult) (*handlers.RequestContext, error) {
+// prepareRequest populates the RequestContext and calls the registered PreRequest plugins
+// for allowing plugging customized logic based on the scheduling results.
+func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestContext, result *schedulingtypes.SchedulingResult) (*handlers.RequestContext, error) {
 	logger := log.FromContext(ctx)
-	// currently only get a single result. Will refactor to pluggably implement the PostSchedule
 	if result == nil || len(result.ProfileResults) == 0 {
 		return reqCtx, errutil.Error{Code: errutil.Internal, Msg: "results must be greater than zero"}
 	}
@@ -192,13 +182,16 @@ func (d *Director) PostDispatch(ctx context.Context, reqCtx *handlers.RequestCon
 	if err != nil {
 		return reqCtx, err
 	}
+	targetPort := int(pool.Spec.TargetPortNumber)
 
-	endpoint := targetPod.Address + ":" + strconv.Itoa(int(pool.Spec.TargetPortNumber))
+	endpoint := net.JoinHostPort(targetPod.Address, strconv.Itoa(targetPort))
 	logger.V(logutil.DEFAULT).Info("Request handled", "model", reqCtx.Model, "targetModel", reqCtx.ResolvedTargetModel, "endpoint", targetPod)
 
 	reqCtx.TargetPod = targetPod
 	reqCtx.TargetEndpoint = endpoint
 
+	d.runPreRequestPlugins(ctx, reqCtx.SchedulingRequest, result, targetPort)
+
 	return reqCtx, nil
 }
 
@@ -254,6 +247,16 @@ func RandomWeightedDraw(logger logr.Logger, model *v1alpha2.InferenceModel, seed
 	return ""
 }
 
+func (d *Director) runPreRequestPlugins(ctx context.Context, request *schedulingtypes.LLMRequest, schedulingResult *schedulingtypes.SchedulingResult,
+	targetPort int) {
+	for _, plugin := range d.preRequestPlugins {
+		log.FromContext(ctx).V(logutil.DEBUG).Info("Running pre-request plugin", "plugin", plugin.Name())
+		before := time.Now()
+		plugin.PreRequest(ctx, request, schedulingResult, targetPort)
+		metrics.RecordRequestControlPluginProcessingLatency(PreRequestPluginType, plugin.Name(), time.Since(before))
+	}
+}
+
 func (d *Director) runPostResponsePlugins(ctx context.Context, request *schedulingtypes.LLMRequest, response *Response, targetPod *backend.Pod) {
 	for _, plugin := range d.postResponsePlugins {
 		log.FromContext(ctx).V(logutil.DEBUG).Info("Running post-response plugin", "plugin", plugin.Name())
diff --git a/pkg/epp/requestcontrol/plugins.go b/pkg/epp/requestcontrol/plugins.go
index 89dd9ad..ba51c2a 100644
--- a/pkg/epp/requestcontrol/plugins.go
+++ b/pkg/epp/requestcontrol/plugins.go
@@ -25,9 +25,17 @@ import (
 )
 
 const (
+	PreRequestPluginType   = "PreRequest"
 	PostResponsePluginType = "PostResponse"
 )
 
+// PreRequest is called by the director after a getting result from scheduling layer and
+// before a request is sent to the selected model server.
+type PreRequest interface {
+	plugins.Plugin
+	PreRequest(ctx context.Context, request *types.LLMRequest, schedulingResult *types.SchedulingResult, targetPort int)
+}
+
 // PostResponse is called by the director after a successful response was sent.
 // The given pod argument is the pod that served the request.
 type PostResponse interface {
diff --git a/pkg/epp/requestcontrol/request_control_config.go b/pkg/epp/requestcontrol/request_control_config.go
index e5f2cf7..9051b36 100644
--- a/pkg/epp/requestcontrol/request_control_config.go
+++ b/pkg/epp/requestcontrol/request_control_config.go
@@ -16,21 +16,40 @@ limitations under the License.
 
 package requestcontrol
 
+import "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+
 // NewConfig creates a new Config object and returns its pointer.
 func NewConfig() *Config {
 	return &Config{
+		preRequestPlugins:   []PreRequest{},
 		postResponsePlugins: []PostResponse{},
 	}
 }
 
 // Config provides a configuration for the requestcontrol plugins.
 type Config struct {
+	preRequestPlugins   []PreRequest
 	postResponsePlugins []PostResponse
 }
 
+// WithPreRequestPlugins sets the given plugins as the PreRequest plugins.
+// If the Config has PreRequest plugins already, this call replaces the existing plugins with the given ones.
+func (c *Config) WithPreRequestPlugins(plugins ...PreRequest) *Config {
+	c.preRequestPlugins = plugins
+	return c
+}
+
 // WithPostResponsePlugins sets the given plugins as the PostResponse plugins.
 // If the Config has PostResponse plugins already, this call replaces the existing plugins with the given ones.
 func (c *Config) WithPostResponsePlugins(plugins ...PostResponse) *Config {
 	c.postResponsePlugins = plugins
 	return c
 }
+
+func (c *Config) AddPlugins(instances map[string]plugins.Plugin) {
+	for _, plugin := range instances {
+		if postResponse, ok := plugin.(PostResponse); ok {
+			c.postResponsePlugins = append(c.postResponsePlugins, postResponse)
+		}
+	}
+}
diff --git a/pkg/epp/scheduling/config/config.go b/pkg/epp/scheduling/config/config.go
index 80efaaa..e7fd0a3 100644
--- a/pkg/epp/scheduling/config/config.go
+++ b/pkg/epp/scheduling/config/config.go
@@ -33,8 +33,8 @@ type Config struct {
 
 const (
 	// Default values for LoRA specific thresholds
-	defaultQueueingThresholdLoRA = 128
-	defaultLoraAffinityThreshold = 0.999
+	DefaultQueueingThresholdLoRA = 128
+	DefaultLoraAffinityThreshold = 0.999
 )
 
 // LoadConfig loads configuration from environment variables
@@ -45,8 +45,8 @@ func LoadConfig() Config {
 	config := Config{
 		KVCacheThreshold:       envutil.GetEnvFloat("KV_CACHE_THRESHOLD", commonconfig.DefaultKVCacheThreshold, baseLogger),
 		QueueThresholdCritical: envutil.GetEnvInt("QUEUE_THRESHOLD_CRITICAL", commonconfig.DefaultQueueThresholdCritical, baseLogger),
-		QueueingThresholdLoRA:  envutil.GetEnvInt("QUEUING_THRESHOLD_LORA", defaultQueueingThresholdLoRA, baseLogger),
-		LoraAffinityThreshold:  envutil.GetEnvFloat("LORA_AFFINITY_THRESHOLD", defaultLoraAffinityThreshold, baseLogger),
+		QueueingThresholdLoRA:  envutil.GetEnvInt("QUEUING_THRESHOLD_LORA", DefaultQueueingThresholdLoRA, baseLogger),
+		LoraAffinityThreshold:  envutil.GetEnvFloat("LORA_AFFINITY_THRESHOLD", DefaultLoraAffinityThreshold, baseLogger),
 	}
 
 	baseLogger.V(logutil.DEFAULT).Info("Scheduler configuration loaded", "config", config)
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 fe18395..c8f34ea 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
@@ -18,15 +18,24 @@ package filter
 
 import (
 	"context"
+	"encoding/json"
 	"math"
 
+	"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"
 )
 
-// compile-time type assertion
+const LeastKVCacheFilterName = "least-KV-cache"
+
+// compile-time type validation
 var _ framework.Filter = &LeastKVCacheFilter{}
 
+// LeastKVCacheFilterFactory is the plugin factory function for the Least KV Cache filter
+func LeastKVCacheFilterFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewLeastKVCacheFilter(), nil
+}
+
 // NewLeastKVCacheFilter initializes a new LeastKVCacheFilter and returns its pointer.
 func NewLeastKVCacheFilter() *LeastKVCacheFilter {
 	return &LeastKVCacheFilter{}
@@ -41,7 +50,7 @@ type LeastKVCacheFilter struct{}
 
 // Name returns the name of the filter.
 func (f *LeastKVCacheFilter) Name() string {
-	return "least-KV-cache"
+	return LeastKVCacheFilterName
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
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 8a43ce7..1a30d12 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
@@ -18,15 +18,24 @@ package filter
 
 import (
 	"context"
+	"encoding/json"
 	"math"
 
+	"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"
 )
 
-// compile-time type assertion
+const LeastQueueFilterName = "least-queue"
+
+// compile-time type validation
 var _ framework.Filter = &LeastQueueFilter{}
 
+// LeastQueueFilterFactory is the plugin factory function for the Least Queue filter
+func LeastQueueFilterFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewLeastQueueFilter(), nil
+}
+
 // NewLeastQueueFilter initializes a new LeastQueueFilter and returns its pointer.
 func NewLeastQueueFilter() *LeastQueueFilter {
 	return &LeastQueueFilter{}
@@ -41,7 +50,7 @@ type LeastQueueFilter struct{}
 
 // Name returns the name of the filter.
 func (f *LeastQueueFilter) Name() string {
-	return "least-queue"
+	return LeastQueueFilterName
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
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 04c0107..1d89e48 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
@@ -18,17 +18,35 @@ package filter
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"math/rand"
 	"time"
 
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"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/types"
 )
 
-// compile-time type assertion
+const LoraAffinityFilterName = "lora-affinity"
+
+type loraAffinityFilterParameters struct {
+	Threshold float64 `json:"threshold"`
+}
+
+// compile-time type validation
 var _ framework.Filter = &LoraAffinityFilter{}
 
+// LoraAffinityFilterFactory is the factory function for the LoraAffinity filter
+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. Error: %s", LoraAffinityFilterName, err)
+	}
+	return &LoraAffinityFilter{loraAffinityThreshold: parameters.Threshold}, nil
+}
+
 // NewLoraAffinityFilter initializes a new LoraAffinityFilter and returns its pointer.
 func NewLoraAffinityFilter() *LoraAffinityFilter {
 	return &LoraAffinityFilter{
@@ -49,7 +67,7 @@ type LoraAffinityFilter struct {
 
 // Name returns the name of the filter.
 func (f *LoraAffinityFilter) Name() string {
-	return "lora-affinity"
+	return LoraAffinityFilterName
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
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 0192c32..9ab0717 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
@@ -18,15 +18,35 @@ package filter
 
 import (
 	"context"
+	"fmt"
 
+	"encoding/json"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"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/types"
 )
 
-// compile-time type assertion
+const LowQueueFilterName = "low-queue"
+
+type lowQueueFilterParameters struct {
+	Threshold int `json:"threshold"`
+}
+
+// compile-time type validation
 var _ framework.Filter = &LowQueueFilter{}
 
+// LowQueueFilterFactory is the factory function for the LowQueue filter
+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. Error: %s", LowQueueFilterName, err)
+	}
+
+	return &LowQueueFilter{queueingThresholdLoRA: parameters.Threshold}, nil
+}
+
 // NewLowQueueFilter initializes a new LowQueueFilter and returns its pointer.
 func NewLowQueueFilter() *LowQueueFilter {
 	return &LowQueueFilter{
@@ -41,7 +61,7 @@ type LowQueueFilter struct {
 
 // Name returns the name of the filter.
 func (f *LowQueueFilter) Name() string {
-	return "low-queue"
+	return LowQueueFilterName
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
index 0d40746..6ee8253 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
@@ -19,12 +19,14 @@ package prefix
 import (
 	"context"
 	"encoding/binary"
+	"encoding/json"
 	"fmt"
 
 	"github.com/cespare/xxhash/v2"
 	k8stypes "k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
+	"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"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
@@ -49,17 +51,19 @@ const (
 	// token is about 128KB in size, so we can cache 500K tokens. Using the default block size of 16
 	// in vLLM, we will have 250K / 16 = 31.25K blocks.
 	DefaultLRUCapacityPerServer = 31250
+
+	PrefixCachePluginName = "prefix-cache"
 )
 
 type Config struct {
 	// The input prompt is broken into sizes of HashBlockSize to calculate block hashes . Requests
 	// with length shorter than the block size will be ignored.
-	HashBlockSize int
+	HashBlockSize int `json:"hashBlockSize"`
 	// MaxPrefixBlocksToMatch is the maximum number of prefix blocks to match. Input beyond this limit will
 	// be ignored.
-	MaxPrefixBlocksToMatch int
+	MaxPrefixBlocksToMatch int `json:"maxPrefixBlocksToMatch"`
 	// Max capacity size of the LRU indexer in number of entries per server (pod).
-	LRUCapacityPerServer int
+	LRUCapacityPerServer int `json:"lruCapacityPerServer"`
 }
 
 type Plugin struct {
@@ -84,7 +88,24 @@ func (s ServerID) String() string {
 	return k8stypes.NamespacedName(s).String()
 }
 
-// compile-time type assertion
+// PrefixCachePluginFactory is the factory for the PrefixCache plugin
+func PrefixCachePluginFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	parameters := Config{
+		HashBlockSize:          DefaultHashBlockSize,
+		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", PrefixCachePluginName, err)
+	}
+
+	return &Plugin{
+		Config:  parameters,
+		indexer: newIndexer(parameters.LRUCapacityPerServer),
+	}, nil
+}
+
+// compile-time type validation
 var _ types.StateData = &schedulingContextState{}
 
 // This is the state of this plugin to be used during a scheduling cycle.
@@ -133,7 +154,7 @@ func New(config Config) *Plugin {
 
 // Name returns the name of the plugin.
 func (m *Plugin) Name() string {
-	return "prefix-cache"
+	return PrefixCachePluginName
 }
 
 // Score returns the scoring result for the given list of pods based on context.
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 1a859f5..f358c16 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
@@ -18,17 +18,26 @@ package picker
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 
 	"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"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
-// compile-time type assertion
+const MaxScorePickerName = "max-score"
+
+// compile-time type validation
 var _ framework.Picker = &MaxScorePicker{}
 
+// MaxScorePickerFactory is the factory for the MaxScore picker
+func MaxScorePickerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return &MaxScorePicker{random: NewRandomPicker()}, nil
+}
+
 // NewMaxScorePicker initializes a new MaxScorePicker and returns its pointer.
 func NewMaxScorePicker() *MaxScorePicker {
 	return &MaxScorePicker{
@@ -43,7 +52,7 @@ type MaxScorePicker struct {
 
 // Name returns the name of the picker.
 func (p *MaxScorePicker) Name() string {
-	return "max_score"
+	return MaxScorePickerName
 }
 
 // Pick selects the pod with the maximum score from the list of candidates.
diff --git a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
index 4f585f5..8dec5e2 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
@@ -18,18 +18,27 @@ package picker
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"math/rand"
 
 	"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"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
-// compile-time type assertion
+const RandomPickerName = "random"
+
+// compile-time type validation
 var _ framework.Picker = &RandomPicker{}
 
+// RandomPickerFactory is the factory for the Random picker
+func RandomPickerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewRandomPicker(), nil
+}
+
 // NewRandomPicker initializes a new RandomPicker and returns its pointer.
 func NewRandomPicker() *RandomPicker {
 	return &RandomPicker{}
@@ -40,7 +49,7 @@ type RandomPicker struct{}
 
 // Name returns the name of the picker.
 func (p *RandomPicker) Name() string {
-	return "random"
+	return RandomPickerName
 }
 
 // Pick selects a random pod from the list of candidates.
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 43bfd62..f31811a 100644
--- a/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
+++ b/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
@@ -18,14 +18,22 @@ package profile
 
 import (
 	"context"
+	"encoding/json"
 
+	"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"
 )
 
+const SingleProfileHandlerName = "single-profile"
+
 // compile-time type assertion
 var _ framework.ProfileHandler = &SingleProfileHandler{}
 
+func SingleProfileHandlerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewSingleProfileHandler(), nil
+}
+
 // NewSingleProfileHandler initializes a new SingleProfileHandler and returns its pointer.
 func NewSingleProfileHandler() *SingleProfileHandler {
 	return &SingleProfileHandler{}
@@ -36,7 +44,7 @@ type SingleProfileHandler struct{}
 
 // Name returns the name of the Profiles Picker.
 func (h *SingleProfileHandler) Name() string {
-	return "single-profile"
+	return SingleProfileHandlerName
 }
 
 // Pick selects the SchedulingProfiles to run from the list of candidate profiles, while taking into consideration the request properties and the
diff --git a/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go b/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
index 1e53660..50e6485 100644
--- a/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
+++ b/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
@@ -18,18 +18,26 @@ package scorer
 
 import (
 	"context"
+	"encoding/json"
 
+	"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"
 )
 
 const (
 	DefaultKVCacheScorerWeight = 1
+	KvCacheScorerName          = "kv-cache"
 )
 
 // compile-time type assertion
 var _ framework.Scorer = &KVCacheScorer{}
 
+// KvCacheScorerFactory is the factory for the KV-Cache scorer
+func KvCacheScorerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return &KVCacheScorer{}, nil
+}
+
 // NewKVCacheScorer initializes a new KVCacheScorer and returns its pointer.
 func NewKVCacheScorer() *KVCacheScorer {
 	return &KVCacheScorer{}
@@ -40,7 +48,7 @@ type KVCacheScorer struct{}
 
 // Name returns the name of the scorer.
 func (s *KVCacheScorer) Name() string {
-	return "kv-cache"
+	return KvCacheScorerName
 }
 
 // Score returns the scoring result for the given list of pods based on context.
diff --git a/pkg/epp/scheduling/framework/plugins/scorer/queue.go b/pkg/epp/scheduling/framework/plugins/scorer/queue.go
index dd31190..0a769b8 100644
--- a/pkg/epp/scheduling/framework/plugins/scorer/queue.go
+++ b/pkg/epp/scheduling/framework/plugins/scorer/queue.go
@@ -18,19 +18,27 @@ package scorer
 
 import (
 	"context"
+	"encoding/json"
 	"math"
 
+	"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"
 )
 
 const (
 	DefaultQueueScorerWeight = 1
+	QueueScorerName          = "queue"
 )
 
 // compile-time type assertion
 var _ framework.Scorer = &QueueScorer{}
 
+// QueueScorerFactory is the factory for the Queue based scorer
+func QueueScorerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return &QueueScorer{}, nil
+}
+
 // NewQueueScorer initializes a new QueueScorer and returns its pointer.
 func NewQueueScorer() *QueueScorer {
 	return &QueueScorer{}
@@ -42,7 +50,7 @@ type QueueScorer struct{}
 
 // Name returns the name of the scorer.
 func (s *QueueScorer) Name() string {
-	return "queue"
+	return QueueScorerName
 }
 
 // Score returns the scoring result for the given list of pods based on context.
diff --git a/pkg/epp/scheduling/scheduler.go b/pkg/epp/scheduling/scheduler.go
index e9536ba..82c8e99 100644
--- a/pkg/epp/scheduling/scheduler.go
+++ b/pkg/epp/scheduling/scheduler.go
@@ -28,7 +28,7 @@ import (
 	"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"
-	profilepicker "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/profile"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
@@ -68,9 +68,9 @@ func NewScheduler(datastore Datastore) *Scheduler {
 		WithFilters(lowLatencyFilter).
 		WithPicker(&picker.RandomPicker{})
 
-	profilePicker := profilepicker.NewSingleProfileHandler()
+	profileHandler := profile.NewSingleProfileHandler()
 
-	return NewSchedulerWithConfig(datastore, NewSchedulerConfig(profilePicker, map[string]*framework.SchedulerProfile{"default": defaultProfile}))
+	return NewSchedulerWithConfig(datastore, NewSchedulerConfig(profileHandler, map[string]*framework.SchedulerProfile{"default": defaultProfile}))
 }
 
 // NewSchedulerWithConfig returns a new scheduler with the given scheduler plugins configuration.
diff --git a/pkg/epp/scheduling/scheduler_config.go b/pkg/epp/scheduling/scheduler_config.go
index 9549d04..4e83aa8 100644
--- a/pkg/epp/scheduling/scheduler_config.go
+++ b/pkg/epp/scheduling/scheduler_config.go
@@ -16,7 +16,16 @@ limitations under the License.
 
 package scheduling
 
-import "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
+import (
+	"errors"
+	"fmt"
+
+	"github.com/go-logr/logr"
+
+	"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/framework"
+)
 
 // NewSchedulerConfig creates a new SchedulerConfig object and returns its pointer.
 func NewSchedulerConfig(profileHandler framework.ProfileHandler, profiles map[string]*framework.SchedulerProfile) *SchedulerConfig {
@@ -31,3 +40,48 @@ type SchedulerConfig struct {
 	profileHandler framework.ProfileHandler
 	profiles       map[string]*framework.SchedulerProfile
 }
+
+func LoadSchedulerConfig(configProfiles []v1alpha1.SchedulingProfile, references map[string]plugins.Plugin,
+	log logr.Logger) (*SchedulerConfig, error) {
+
+	var profiles = map[string]*framework.SchedulerProfile{}
+
+	for _, configProfile := range configProfiles {
+		profile := framework.SchedulerProfile{}
+
+		for _, plugin := range configProfile.Plugins {
+			var err error
+			thePlugin := references[plugin.PluginRef]
+			if theScorer, ok := thePlugin.(framework.Scorer); ok {
+				if plugin.Weight == nil {
+					err = fmt.Errorf("scorer %s is missing a weight", plugin.PluginRef)
+					log.Error(err, "failed to instantiate scheduler profile")
+					return nil, err
+				}
+				thePlugin = framework.NewWeightedScorer(theScorer, *plugin.Weight)
+			}
+			err = profile.AddPlugins(thePlugin)
+			if err != nil {
+				return nil, err
+			}
+		}
+		profiles[configProfile.Name] = &profile
+	}
+
+	var profileHandler framework.ProfileHandler
+	var profileHandlerName string
+
+	for pluginName, thePlugin := range references {
+		if theProfileHandler, ok := thePlugin.(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)
+			}
+			profileHandler = theProfileHandler
+			profileHandlerName = pluginName
+		}
+	}
+	if profileHandler != nil {
+		return NewSchedulerConfig(profileHandler, profiles), nil
+	}
+	return nil, errors.New("no profile handler was specified")
+}
diff --git a/site-src/guides/conformance-tests.md b/site-src/guides/conformance-tests.md
new file mode 100644
index 0000000..aa88438
--- /dev/null
+++ b/site-src/guides/conformance-tests.md
@@ -0,0 +1,46 @@
+
+# Test Setup and Execution
+
+This document provides steps to run the Gateway API Inference Extension conformance tests.
+
+## Prerequisites
+
+1.  You need a Kubernetes cluster with [LoadBalancer](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) support.
+
+2.  Choose an Implementation -
+Install an [existing implementation](https://gateway-api-inference-extension.sigs.k8s.io/implementations/gateways/). For setup instructions, refer to the [The Quickstart Guide](https://gateway-api-inference-extension.sigs.k8s.io/guides/).  Alternatively run tests against your implementation after completing the [implementer's guide](https://gateway-api-inference-extension.sigs.k8s.io/guides/implementers/#implementers-guide).
+
+Note: Since the EPP (EndPoint Picker) takes the `InferencePool` name as an environment variable, each conformance test creates a corresponding EPP deployment for each `InferencePool` it defines. For conformance testing, the EPP is configured with the `HeaderBasedTestingFilter`. This is enabled by setting the `ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING=true` environment variable in the EPP deployment manifest. 
+
+## Running Conformance Tests
+
+1.  **Clone the Repository**:
+    Create a local copy of the Gateway API Inference Extension repository:
+    ```bash
+    git clone https://github.com/kubernetes-sigs/gateway-api-inference-extension.git
+    cd gateway-api-inference-extension
+    ```
+
+2.  **Execute Tests**:
+    Run the following command to execute all available tests. Replace `<your_gatewayclass_name>` with the GatewayClass used by the implementation under test.
+
+    ```bash
+    go test ./conformance -args -gateway-class <your_gatewayclass_name>
+    ```
+
+### Test Execution Options
+
+* **Speeding up Reruns**: For repeated runs, you can add the flag `-cleanup-base-resources=false`. This will preserve resources such as namespaces and gateways between test runs, speeding up the process.
+    ```bash
+    go test ./conformance -args -gateway-class <your_gatewayclass_name> -cleanup-base-resources=false
+    ```
+
+* **Running Specific Tests**: To run a specific test, you can reference the test name by using the `-run-test` flag. For example:
+    ```bash
+    go test ./conformance -args -gateway-class <your_gatewayclass_name> -run-test HTTPRouteMultipleGatewaysDifferentPools
+    ```
+
+* **Detailed Logging**: To view detailed logs, you can enable logging mode by adding the `-v` as well as `-debug` flags.
+    ```bash
+    go test -v ./conformance -args -debug -gateway-class <your_gatewayclass_name> -cleanup-base-resources=false -run-test HTTPRouteMultipleGatewaysDifferentPools
+    ```
diff --git a/site-src/guides/implementers.md b/site-src/guides/implementers.md
index 7bfd536..204c417 100644
--- a/site-src/guides/implementers.md
+++ b/site-src/guides/implementers.md
@@ -110,4 +110,4 @@ Here are some tips for testing your controller end-to-end:
 
 ### Conformance Tests
 
-A set of conformance tests will be developed soon to help verify that a controller is working as expected. This guide will be updated once we have more information. Stay tuned!
+See [Conformance Test Setup and Execution](https://gateway-api-inference-extension.sigs.k8s.io/guides/conformance-tests).



Test patch:
diff --git a/conformance/tests/basic/httproute_invalid_inferencepool_ref.go b/conformance/tests/basic/httproute_invalid_inferencepool_ref.go
index 967df35..5fc2a66 100644
--- a/conformance/tests/basic/httproute_invalid_inferencepool_ref.go
+++ b/conformance/tests/basic/httproute_invalid_inferencepool_ref.go
@@ -47,7 +47,7 @@ var HTTPRouteInvalidInferencePoolRef = suite.ConformanceTest{
 			appBackendNamespace = "gateway-conformance-app-backend"
 			infraNamespace      = "gateway-conformance-infra"
 			routeName           = "httproute-to-non-existent-pool"
-			gatewayName         = "conformance-gateway"
+			gatewayName         = "conformance-primary-gateway"
 		)
 		routeNN := types.NamespacedName{Name: routeName, Namespace: appBackendNamespace}
 		gatewayNN := types.NamespacedName{Name: gatewayName, Namespace: infraNamespace}
diff --git a/conformance/tests/basic/httproute_invalid_inferencepool_ref.yaml b/conformance/tests/basic/httproute_invalid_inferencepool_ref.yaml
index 61bdb54..227aeef 100644
--- a/conformance/tests/basic/httproute_invalid_inferencepool_ref.yaml
+++ b/conformance/tests/basic/httproute_invalid_inferencepool_ref.yaml
@@ -16,7 +16,7 @@ spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
-    name: conformance-gateway # Name of the shared Gateway from base manifests
+    name: conformance-primary-gateway # Name of the shared Gateway from base manifests
     namespace: gateway-conformance-infra  # Namespace of the shared Gateway
     sectionName: http
   rules:
@@ -24,7 +24,6 @@ spec:
     - group: inference.networking.x-k8s.io
       kind: InferencePool
       name: non-existent-inference-pool # Intentionally Non-Existing
-      port: 8080
     matches:
     - path:
         type: PathPrefix
diff --git a/conformance/tests/basic/inferencepool_accepted.go b/conformance/tests/basic/inferencepool_accepted.go
index d74f738..f34c5fc 100644
--- a/conformance/tests/basic/inferencepool_accepted.go
+++ b/conformance/tests/basic/inferencepool_accepted.go
@@ -47,7 +47,7 @@ var InferencePoolAccepted = suite.ConformanceTest{
 	},
 	Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
 		// created by the associated manifest file.
-		poolNN := types.NamespacedName{Name: "inferencepool-basic-accepted", Namespace: "gateway-conformance-app-backend"}
+		poolNN := types.NamespacedName{Name: "primary-inference-pool", Namespace: "gateway-conformance-app-backend"}
 
 		t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) {
 			// Define the expected status condition. We use the standard "Accepted"
diff --git a/conformance/tests/basic/inferencepool_accepted.yaml b/conformance/tests/basic/inferencepool_accepted.yaml
index ecee5b3..8381500 100644
--- a/conformance/tests/basic/inferencepool_accepted.yaml
+++ b/conformance/tests/basic/inferencepool_accepted.yaml
@@ -1,72 +1,3 @@
-# Basic InferencePool for acceptance testing.
-# This manifest defines the minimal required fields to create valid
-# InferencePool and HTTPRoute resources, which the InferencePoolAccepted
-# test will use to verify that the controller recognizes and accepts the resource.
-
-# --- Minimal Backend Deployment (using agnhost echo server) ---
-# This Deployment provides Pods for the InferencePool to select.
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: infra-backend-v1-deployment
-  namespace: gateway-conformance-app-backend
-  labels:
-    app: infra-backend-v1
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: infra-backend-v1
-  template:
-    metadata:
-      labels:
-        app: infra-backend-v1
-    spec:
-      containers:
-      - name: agnhost-echo
-        image: k8s.gcr.io/e2e-test-images/agnhost:2.39
-        args:
-        - serve-hostname
-        - --http-port=8080
-        ports:
-        - name: http
-          containerPort: 8080
-        readinessProbe:
-          httpGet:
-            path: /
-            port: 8080
-          initialDelaySeconds: 3
-          periodSeconds: 5
-          failureThreshold: 2
-
----
-# --- InferencePool Definition ---
-apiVersion: inference.networking.x-k8s.io/v1alpha2
-kind: InferencePool
-metadata:
-  # This name must match the 'poolNN' variable defined in the
-  # conformance/tests/basic/inferencepool_accepted.go test file.
-  name: inferencepool-basic-accepted
-  # This namespace should be one created by the base manifests.
-  namespace: gateway-conformance-app-backend
-spec:
-  # --- Selector (Required) ---
-  # Selects the Pods belonging to this pool.
-  selector:
-    app: "infra-backend-v1"
-
-  # --- Target Port (Required) ---
-  # The port the model server container (agnhost in this case) listens on.
-  targetPortNumber: 8080 # Matches agnhost's http-port
-
-  # --- Extension Reference ---
-  # GKE-specific configuration reference.
-  extensionRef:
-    # group: "" # Optional
-    # kind: Service # Optional
-    name: infra-backend-v1-epp
-
----
 # --- HTTPRoute Definition ---
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
@@ -77,16 +8,15 @@ spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
-    name: conformance-gateway # Name of the shared Gateway from maniffests.yaml
+    name: conformance-primary-gateway # Name of the shared Gateway from maniffests.yaml
     namespace: gateway-conformance-infra  # Namespace of the shared Gateway
     sectionName: http
   rules:
   - backendRefs:
     - group: inference.networking.x-k8s.io # InferencePool API group
       kind: InferencePool
-      name: inferencepool-basic-accepted # Name of the InferencePool this route points to
+      name: primary-inference-pool # Name of the InferencePool this route points to
       # namespace: gateway-conformance-app-backend - is omitted since it is in the same namespace as HTTPRoute
-      port: 8080 # Matching the InferencePool's targetPortNumber
     matches:
     - path:
         type: PathPrefix
diff --git a/conformance/tests/basic/inferencepool_httproute_port_validation.go b/conformance/tests/basic/inferencepool_httproute_port_validation.go
new file mode 100644
index 0000000..888b659
--- /dev/null
+++ b/conformance/tests/basic/inferencepool_httproute_port_validation.go
@@ -0,0 +1,119 @@
+/*
+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 basic
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+	"sigs.k8s.io/gateway-api/pkg/features"
+
+	// Local project imports
+	"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
+	k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes"
+	trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic"
+)
+
+func init() {
+	tests.ConformanceTests = append(tests.ConformanceTests, InferencePoolHTTPRoutePortValidation)
+}
+
+var InferencePoolHTTPRoutePortValidation = suite.ConformanceTest{
+	ShortName:   "InferencePoolHTTPRoutePortValidation",
+	Description: "Validates HTTPRoute backendRef port configurations (unspecified, matching, non-matching) when referencing an InferencePool, and checks resulting status conditions.",
+	Manifests:   []string{"tests/basic/inferencepool_httproute_port_validation.yaml"},
+	Features: []features.FeatureName{
+		features.FeatureName("SupportInferencePool"),
+		features.SupportGateway,
+	},
+	Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
+		const (
+			appBackendNamespace   = "gateway-conformance-app-backend"
+			infraNamespace        = "gateway-conformance-infra"
+			gatewayName           = "conformance-gateway"
+			poolName              = "target-pool-port-validation"
+			backendDeploymentName = "infra-backend-deployment-port-test"
+		)
+
+		gatewayNN := types.NamespacedName{Name: gatewayName, Namespace: infraNamespace}
+		poolNN := types.NamespacedName{Name: poolName, Namespace: appBackendNamespace}
+
+		gatewayAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gatewayNN)
+
+		t.Run("Scenario 1: HTTPRoute backendRef to InferencePool with Port Unspecified", func(t *testing.T) {
+			routeNN := types.NamespacedName{Name: "httproute-pool-port-unspecified", Namespace: appBackendNamespace}
+			hostname := "port-unspecified.example.com"
+			path := "/test-port-unspecified"
+
+			k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, routeNN, gatewayNN)
+			k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN)
+
+			trafficutils.MakeRequestAndExpectSuccess(
+				t,
+				s.RoundTripper,
+				s.TimeoutConfig,
+				gatewayAddr,
+				hostname,
+				path,
+				backendDeploymentName,
+				appBackendNamespace,
+			)
+		})
+
+		t.Run("Scenario 2: HTTPRoute backendRef to InferencePool with Port Specified and Matching", func(t *testing.T) {
+			routeNN := types.NamespacedName{Name: "httproute-pool-port-matching", Namespace: appBackendNamespace}
+			hostname := "port-matching.example.com"
+			path := "/test-port-matching"
+
+			k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, routeNN, gatewayNN)
+			k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN)
+
+			trafficutils.MakeRequestAndExpectSuccess(
+				t,
+				s.RoundTripper,
+				s.TimeoutConfig,
+				gatewayAddr,
+				hostname,
+				path,
+				backendDeploymentName,
+				appBackendNamespace,
+			)
+		})
+
+		// TODO: Add a warning check after the required change is made per discussion in github.com/kubernetes-sigs/gateway-api-inference-extension/discussions/918
+		t.Run("Scenario 3: HTTPRoute backendRef to InferencePool with Port Specified and Non-Matching. Request still passing because HTTP Port is ignored when inferencePool is backendRef", func(t *testing.T) {
+			routeNN := types.NamespacedName{Name: "httproute-pool-port-non-matching", Namespace: appBackendNamespace}
+			hostname := "port-non-matching.example.com"
+			path := "/test-port-non-matching"
+
+			k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, routeNN, gatewayNN)
+			k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, poolNN)
+
+			trafficutils.MakeRequestAndExpectSuccess(
+				t,
+				s.RoundTripper,
+				s.TimeoutConfig,
+				gatewayAddr,
+				hostname,
+				path,
+				backendDeploymentName,
+				appBackendNamespace,
+			)
+		})
+	},
+}
diff --git a/conformance/tests/basic/inferencepool_httproute_port_validation.yaml b/conformance/tests/basic/inferencepool_httproute_port_validation.yaml
new file mode 100644
index 0000000..d15d317
--- /dev/null
+++ b/conformance/tests/basic/inferencepool_httproute_port_validation.yaml
@@ -0,0 +1,244 @@
+# conformance/tests/basic/inferencepool_httproute_port_validation.yaml
+
+# --- Backend Deployment (reusing standard echoserver) ---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: infra-backend-deployment-port-test
+  namespace: gateway-conformance-app-backend
+  labels:
+    app: infra-backend-port-test
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: infra-backend-port-test
+  template:
+    metadata:
+      labels:
+        app: infra-backend-port-test
+    spec:
+      containers:
+      - name: echoserver
+        image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
+        ports:
+        - containerPort: 3000
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 3000
+          initialDelaySeconds: 3
+          periodSeconds: 5
+          failureThreshold: 2
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+---
+# --- Backend Service ---
+# Service for the infra-backend-deployment-port-test.
+apiVersion: v1
+kind: Service
+metadata:
+  name: infra-backend-svc-port-test
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: infra-backend-port-test
+  ports:
+  - name: http
+    port: 3000
+    targetPort: 3000
+---
+# --- InferencePool Definition ---
+apiVersion: inference.networking.x-k8s.io/v1alpha2
+kind: InferencePool
+metadata:
+  name: target-pool-port-validation
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: "infra-backend-port-test"
+  targetPortNumber: 3000
+  extensionRef:
+    name: target-pool-port-validation-epp
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: target-pool-port-validation-epp
+  namespace: gateway-conformance-app-backend
+spec:
+  selector:
+    app: target-pool-port-validation-epp
+  ports:
+    - protocol: TCP
+      port: 9002
+      targetPort: 9002
+      appProtocol: http2
+  type: ClusterIP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: target-pool-port-validation-epp
+  namespace: gateway-conformance-app-backend
+  labels:
+    app: target-pool-port-validation-epp
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: target-pool-port-validation-epp
+  template:
+    metadata:
+      labels:
+        app: target-pool-port-validation-epp
+    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
+            - "target-pool-port-validation"
+            - "-poolNamespace"
+            - "gateway-conformance-app-backend"
+            - -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
+---
+# --- HTTPRoute Scenario 1: Port Unspecified ---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: httproute-pool-port-unspecified
+  namespace: gateway-conformance-app-backend
+spec:
+  parentRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: conformance-gateway
+    namespace: gateway-conformance-infra
+    sectionName: http
+  hostnames:
+  - "port-unspecified.example.com"
+  rules:
+  - backendRefs:
+    - group: inference.networking.x-k8s.io
+      kind: InferencePool
+      name: target-pool-port-validation
+      # Port is intentionally unspecified here
+    matches:
+    - path:
+        type: PathPrefix
+        value: /test-port-unspecified
+---
+# --- HTTPRoute Scenario 2: Port Matching ---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: httproute-pool-port-matching
+  namespace: gateway-conformance-app-backend
+spec:
+  parentRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: conformance-gateway
+    namespace: gateway-conformance-infra
+    sectionName: http
+  hostnames:
+  - "port-matching.example.com"
+  rules:
+  - backendRefs:
+    - group: inference.networking.x-k8s.io
+      kind: InferencePool
+      name: target-pool-port-validation
+      port: 3000 # Port matches InferencePool's targetPortNumber
+    matches:
+    - path:
+        type: PathPrefix
+        value: /test-port-matching
+---
+# --- HTTPRoute Scenario 3: Port Non-Matching ---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: httproute-pool-port-non-matching
+  namespace: gateway-conformance-app-backend
+spec:
+  parentRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: conformance-gateway
+    namespace: gateway-conformance-infra
+    sectionName: http
+  hostnames:
+  - "port-non-matching.example.com"
+  rules:
+  - backendRefs:
+    - group: inference.networking.x-k8s.io
+      kind: InferencePool
+      name: target-pool-port-validation
+      port: 8888 # Port does NOT match InferencePool's targetPortNumber
+    matches:
+    - path:
+        type: PathPrefix
+        value: /test-port-non-matching
+---
+# --- Conformance EPP Requried Role and RoleBindings ---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: inference-model-reader
+  namespace: gateway-conformance-app-backend
+rules:
+- apiGroups: ["inference.networking.x-k8s.io"]
+  resources: ["inferencemodels", "inferencepools"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["pods"]
+  verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: epp-to-inference-model-reader
+  namespace: gateway-conformance-app-backend
+subjects:
+- kind: ServiceAccount
+  name: default
+  namespace: gateway-conformance-app-backend
+roleRef:
+  kind: Role
+  name: inference-model-reader
+  apiGroup: rbac.authorization.k8s.io
diff --git a/conformance/tests/basic/inferencepool_invalid_epp_service.go b/conformance/tests/basic/inferencepool_invalid_epp_service.go
index cfbeb28..73e6dc0 100644
--- a/conformance/tests/basic/inferencepool_invalid_epp_service.go
+++ b/conformance/tests/basic/inferencepool_invalid_epp_service.go
@@ -52,7 +52,7 @@ var InferencePoolInvalidEPPService = suite.ConformanceTest{
 		)
 
 		routeNN := types.NamespacedName{Name: "httproute-for-invalid-epp-pool", Namespace: appNamespace}
-		gwNN := types.NamespacedName{Name: "conformance-gateway", Namespace: infraNamespace}
+		gwNN := types.NamespacedName{Name: "conformance-primary-gateway", Namespace: infraNamespace}
 
 		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, s.Client, s.TimeoutConfig, s.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
 
diff --git a/conformance/tests/basic/inferencepool_invalid_epp_service.yaml b/conformance/tests/basic/inferencepool_invalid_epp_service.yaml
index 00941bb..3a19036 100644
--- a/conformance/tests/basic/inferencepool_invalid_epp_service.yaml
+++ b/conformance/tests/basic/inferencepool_invalid_epp_service.yaml
@@ -17,14 +17,13 @@ metadata:
   namespace: gateway-conformance-app-backend
 spec:
   parentRefs:
-  - name: conformance-gateway
+  - name: conformance-primary-gateway
     namespace: gateway-conformance-infra
   rules:
   - backendRefs:
     - name: pool-with-invalid-epp
       kind: InferencePool
       group: inference.networking.x-k8s.io
-      port: 80
     matches:
     - path:
         type: PathPrefix
diff --git a/conformance/tests/basic/inferencepool_multiple_rules_different_pools.go b/conformance/tests/basic/inferencepool_multiple_rules_different_pools.go
index a3ff986..d91a2fa 100644
--- a/conformance/tests/basic/inferencepool_multiple_rules_different_pools.go
+++ b/conformance/tests/basic/inferencepool_multiple_rules_different_pools.go
@@ -47,13 +47,13 @@ var HTTPRouteMultipleRulesDifferentPools = suite.ConformanceTest{
 			appBackendNamespace = "gateway-conformance-app-backend"
 			infraNamespace      = "gateway-conformance-infra"
 
-			poolPrimaryName   = "pool-primary"
-			poolSecondaryName = "pool-secondary"
+			poolPrimaryName   = "primary-inference-pool"
+			poolSecondaryName = "secondary-inference-pool"
 			routeName         = "httproute-multiple-rules-different-pools"
-			gatewayName       = "conformance-gateway"
+			gatewayName       = "conformance-primary-gateway"
 
-			backendPrimaryLabelValue   = "inference-model-1"
-			backendSecondaryLabelValue = "inference-model-2"
+			backendPrimaryLabelValue   = "primary-inference-model-server"
+			backendSecondaryLabelValue = "secondary-inference-model-server"
 			backendAppLabelKey         = "app"
 
 			primaryPath   = "/primary"
diff --git a/conformance/tests/basic/inferencepool_multiple_rules_different_pools.yaml b/conformance/tests/basic/inferencepool_multiple_rules_different_pools.yaml
index de34274..7ef7ced 100644
--- a/conformance/tests/basic/inferencepool_multiple_rules_different_pools.yaml
+++ b/conformance/tests/basic/inferencepool_multiple_rules_different_pools.yaml
@@ -1,125 +1,4 @@
 ---
-apiVersion: v1
-kind: Service
-metadata:
-  name: pool-primary-epp
-  namespace: gateway-conformance-app-backend
-spec:
-  selector:
-    app: pool-primary-epp
-  ports:
-  - name: grpc
-    port: 9002
-    targetPort: 9002
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: pool-primary-epp
-  namespace: gateway-conformance-app-backend
-  labels:
-    app: pool-primary-epp
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: pool-primary-epp
-  template:
-    metadata:
-      labels:
-        app: pool-primary-epp
-    spec:
-      containers:
-      - name: epp
-        # TODO(#996) Switch to use a released version of the image instead of epp:main
-        image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
-        imagePullPolicy: Always
-        env:
-        - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING
-          value: "true"
-        args:
-        - "-poolName"
-        - "pool-primary"
-        - "-poolNamespace"
-        - "gateway-conformance-app-backend"
-        - -grpcPort
-        - "9002"
-        - -grpcHealthPort
-        - "9003"
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: pool-secondary-epp
-  namespace: gateway-conformance-app-backend
-spec:
-  selector:
-    app: pool-secondary-epp
-  ports:
-  - name: grpc
-    port: 9002
-    targetPort: 9002
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: pool-secondary-epp
-  namespace: gateway-conformance-app-backend
-  labels:
-    app: pool-secondary-epp
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: pool-secondary-epp
-  template:
-    metadata:
-      labels:
-        app: pool-secondary-epp
-    spec:
-      containers:
-      - name: epp
-        image: us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/epp:main
-        imagePullPolicy: Always
-        env:
-        - name: ENABLE_REQ_HEADER_BASED_SCHEDULER_FOR_TESTING
-          value: "true"
-        args:
-        - "-poolName"
-        - "pool-secondary"
-        - "-poolNamespace"
-        - "gateway-conformance-app-backend"
-        - -grpcPort
-        - "9002"
-        - -grpcHealthPort
-        - "9003"
----
-apiVersion: inference.networking.x-k8s.io/v1alpha2
-kind: InferencePool
-metadata:
-  name: pool-primary
-  namespace: gateway-conformance-app-backend
-spec:
-  selector:
-    app: inference-model-1
-  targetPortNumber: 3000
-  extensionRef:
-    name: pool-primary-epp
-    portNumber: 9002
----
-apiVersion: inference.networking.x-k8s.io/v1alpha2
-kind: InferencePool
-metadata:
-  name: pool-secondary
-  namespace: gateway-conformance-app-backend
-spec:
-  selector:
-    app: inference-model-2
-  targetPortNumber: 3000
-  extensionRef:
-    name: pool-secondary-epp
-    portNumber: 9002
----
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
@@ -127,7 +6,7 @@ metadata:
   namespace: gateway-conformance-app-backend
 spec:
   parentRefs:
-    - name: conformance-gateway
+    - name: conformance-primary-gateway
       namespace: gateway-conformance-infra
   rules:
     - matches:
@@ -135,16 +14,14 @@ spec:
             type: PathPrefix
             value: /primary
       backendRefs:
-        - name: pool-primary
+        - name: primary-inference-pool
           kind: InferencePool
           group: inference.networking.x-k8s.io
-          port: 80
     - matches:
         - path:
             type: PathPrefix
             value: /secondary
       backendRefs:
-        - name: pool-secondary
+        - name: secondary-inference-pool
           kind: InferencePool
           group: inference.networking.x-k8s.io
-          port: 80
diff --git a/conformance/tests/basic/inferencepool_resolvedrefs_condition.go b/conformance/tests/basic/inferencepool_resolvedrefs_condition.go
index 1bb8c7f..bf9dd12 100644
--- a/conformance/tests/basic/inferencepool_resolvedrefs_condition.go
+++ b/conformance/tests/basic/inferencepool_resolvedrefs_condition.go
@@ -50,8 +50,8 @@ var InferencePoolParentStatus = suite.ConformanceTest{
 		const (
 			appBackendNamespace        = "gateway-conformance-app-backend"
 			infraNamespace             = "gateway-conformance-infra"
-			poolName                   = "multi-gateway-pool"
-			sharedPrimaryGatewayName   = "conformance-gateway"
+			poolName                   = "primary-inference-pool"
+			sharedPrimaryGatewayName   = "conformance-primary-gateway"
 			sharedSecondaryGatewayName = "conformance-secondary-gateway"
 			httpRoutePrimaryName       = "httproute-for-primary-gw"
 			httpRouteSecondaryName     = "httproute-for-secondary-gw"
@@ -59,7 +59,7 @@ var InferencePoolParentStatus = suite.ConformanceTest{
 			pathPrimaryGw              = "/primary-gateway-test"
 			hostnameSecondaryGw        = "secondary.example.com"
 			pathSecondaryGw            = "/secondary-gateway-test"
-			backendServicePodName      = "infra-backend-deployment"
+			backendServicePodName      = "primary-inference-model-server-deployment"
 		)
 
 		poolNN := types.NamespacedName{Name: poolName, Namespace: appBackendNamespace}
diff --git a/conformance/tests/basic/inferencepool_resolvedrefs_condition.yaml b/conformance/tests/basic/inferencepool_resolvedrefs_condition.yaml
index 4b58f67..416352e 100644
--- a/conformance/tests/basic/inferencepool_resolvedrefs_condition.yaml
+++ b/conformance/tests/basic/inferencepool_resolvedrefs_condition.yaml
@@ -3,82 +3,7 @@
 # This manifest defines the initial resources for the
 # inferencepool_resolvedrefs_condition.go conformance test.
 
-# --- Backend Deployment (using standard Gateway API echoserver) ---
-# This Deployment provides Pods for the InferencePool to select.
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: infra-backend-deployment
-  namespace: gateway-conformance-app-backend
-  labels:
-    app: infra-backend
-spec:
-  selector:
-    matchLabels:
-      app: infra-backend
-  template:
-    metadata:
-      labels:
-        app: infra-backend
-    spec:
-      containers:
-      - name: echoserver
-        image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
-        ports:
-        - containerPort: 3000
-        readinessProbe:
-          httpGet:
-            path: /
-            port: 3000
-          initialDelaySeconds: 3
-          periodSeconds: 5
-          failureThreshold: 2
-        env:
-        - name: POD_NAME
-          valueFrom:
-            fieldRef:
-              fieldPath: metadata.name
-        - name: NAMESPACE
-          valueFrom:
-            fieldRef:
-              fieldPath: metadata.namespace
-        - name: POD_IP
-          valueFrom:
-            fieldRef:
-              fieldPath: status.podIP
----
-# --- Backend Service ---
-# Service for the infra-backend-deployment.
-apiVersion: v1
-kind: Service
-metadata:
-  name: infra-backend-svc
-  namespace: gateway-conformance-app-backend
-spec:
-  selector:
-    app: infra-backend
-  ports:
-  - name: http
-    port: 3000
-    targetPort: 3000
-  - name: epp
-    port: 9002
-    targetPort: 9002
----
-# --- InferencePool Definition ---
-apiVersion: inference.networking.x-k8s.io/v1alpha2
-kind: InferencePool
-metadata:
-  name: multi-gateway-pool
-  namespace: gateway-conformance-app-backend
-spec:
-  selector:
-    app: "infra-backend"
-  targetPortNumber: 3000
-  extensionRef:
-    name: infra-backend-svc
----
-# --- HTTPRoute for Primary Gateway (conformance-gateway) ---
+# --- HTTPRoute for Primary Gateway (conformance-primary-gateway) ---
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
@@ -88,7 +13,7 @@ spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
-    name: conformance-gateway
+    name: conformance-primary-gateway
     namespace: gateway-conformance-infra
     sectionName: http
   hostnames:
@@ -97,8 +22,7 @@ spec:
   - backendRefs:
     - group: inference.networking.x-k8s.io
       kind: InferencePool
-      name: multi-gateway-pool
-      port: 3000
+      name: primary-inference-pool
     matches:
     - path:
         type: PathPrefix
@@ -123,8 +47,7 @@ spec:
   - backendRefs:
     - group: inference.networking.x-k8s.io
       kind: InferencePool
-      name: multi-gateway-pool
-      port: 3000
+      name: primary-inference-pool
     matches:
     - path:
         type: PathPrefix
diff --git a/pkg/epp/common/config/configloader_test.go b/pkg/epp/common/config/configloader_test.go
new file mode 100644
index 0000000..7600ba5
--- /dev/null
+++ b/pkg/epp/common/config/configloader_test.go
@@ -0,0 +1,548 @@
+/*
+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 config
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	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/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+)
+
+const (
+	testProfileHandlerName = "test-profile-handler"
+	test1Name              = "test-one"
+	test2Name              = "test-two"
+	testPickerName         = "test-picker"
+)
+
+func TestLoadConfiguration(t *testing.T) {
+	test2Weight := 50
+
+	registerTestPlugins()
+
+	goodConfig := &configapi.EndpointPickerConfig{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "EndpointPickerConfig",
+			APIVersion: "inference.networking.x-k8s.io/v1alpha1",
+		},
+		Plugins: []configapi.PluginSpec{
+			{
+				Name:       "test1",
+				PluginName: test1Name,
+				Parameters: json.RawMessage("{\"threshold\":10}"),
+			},
+			{
+				Name:       "profileHandler",
+				PluginName: "test-profile-handler",
+			},
+			{
+				Name:       test2Name,
+				PluginName: test2Name,
+				Parameters: json.RawMessage("{\"hashBlockSize\":32}"),
+			},
+			{
+				Name:       "testPicker",
+				PluginName: testPickerName,
+			},
+		},
+		SchedulingProfiles: []configapi.SchedulingProfile{
+			{
+				Name: "default",
+				Plugins: []configapi.SchedulingPlugin{
+					{
+						PluginRef: "test1",
+					},
+					{
+						PluginRef: "test-two",
+						Weight:    &test2Weight,
+					},
+					{
+						PluginRef: "testPicker",
+					},
+				},
+			},
+		},
+	}
+
+	tests := []struct {
+		name       string
+		configText string
+		configFile string
+		want       *configapi.EndpointPickerConfig
+		wantErr    bool
+	}{
+		{
+			name:       "success",
+			configText: successConfigText,
+			configFile: "",
+			want:       goodConfig,
+			wantErr:    false,
+		},
+		{
+			name:       "errorBadYaml",
+			configText: errorBadYamlText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorNoProfileHandler",
+			configText: errorNoProfileHandlerText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorBadPluginReferenceText",
+			configText: errorBadPluginReferenceText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorBadPluginReferencePluginText",
+			configText: errorBadPluginReferencePluginText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorNoProfiles",
+			configText: errorNoProfilesText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorNoProfileName",
+			configText: errorNoProfileNameText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorNoProfilePlugins",
+			configText: errorNoProfilePluginsText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorBadProfilePlugin",
+			configText: errorBadProfilePluginText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorBadProfilePluginRef",
+			configText: errorBadProfilePluginRefText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorBadProfilePluginName",
+			configText: errorBadProfilePluginNameText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorDuplicatePlugin",
+			configText: errorDuplicatePluginText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "errorDuplicateProfile",
+			configText: errorDuplicateProfileText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "successFromFile",
+			configText: "",
+			configFile: "../../../../test/testdata/configloader_1_test.yaml",
+			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)
+		if err != nil {
+			if !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 {
+			if test.wantErr {
+				t.Fatalf("In test %s LoadConfig did not return an expected error", test.name)
+			}
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("In test %s LoadConfig returned unexpected response, diff(-want, +got): %v", test.name, diff)
+			}
+		}
+	}
+}
+
+func TestLoadPluginReferences(t *testing.T) {
+	theConfig, err := LoadConfig([]byte(successConfigText), "")
+	if err != nil {
+		t.Fatalf("LoadConfig returned unexpected error: %v", err)
+	}
+	references, err := LoadPluginReferences(theConfig.Plugins, testHandle{})
+	if err != nil {
+		t.Fatalf("LoadPluginReferences returned unexpected error: %v", err)
+	}
+	if len(references) == 0 {
+		t.Fatalf("LoadPluginReferences returned an empty set of references")
+	}
+	if t1, ok := references["test1"]; !ok {
+		t.Fatalf("LoadPluginReferences returned references did not contain test1")
+	} else if _, ok := t1.(*test1); !ok {
+		t.Fatalf("LoadPluginReferences returned references value for 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, testHandle{})
+	if err == nil {
+		t.Fatalf("LoadPluginReferences did not return the expected error")
+	}
+}
+
+func TestInstantiatePlugin(t *testing.T) {
+	plugSpec := configapi.PluginSpec{PluginName: "plover"}
+	_, err := InstantiatePlugin(plugSpec, testHandle{})
+	if err == nil {
+		t.Fatalf("InstantiatePlugin did not return the expected error")
+	}
+}
+
+type testHandle struct {
+}
+
+// The following multi-line string constants, cause false positive lint errors (dupword)
+
+//nolint:dupword
+const successConfigText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: profileHandler
+  pluginName: test-profile-handler
+- pluginName: test-two
+  parameters:
+    hashBlockSize: 32
+- name: testPicker
+  pluginName: test-picker
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: test1
+  - pluginRef: test-two
+    weight: 50
+  - pluginRef: testPicker
+`
+
+//nolint:dupword
+const errorBadYamlText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- testing 1 2 3
+`
+
+//nolint:dupword
+const errorBadPluginReferenceText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- parameters:
+    a: 1234
+`
+
+//nolint:dupword
+const errorBadPluginReferencePluginText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: testx
+  pluginName: test-x
+- name: profileHandler
+  pluginName: test-profile-handler
+`
+
+//nolint:dupword
+const errorNoProfileHandlerText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+schedulingProfiles:
+- name: default
+`
+
+//nolint:dupword
+const errorNoProfilesText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: profileHandler
+  pluginName: test-profile-handler
+`
+
+//nolint:dupword
+const errorNoProfileNameText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- plugins:
+  - pluginRef: test1
+`
+
+//nolint:dupword
+const errorNoProfilePluginsText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+`
+
+//nolint:dupword
+const errorBadProfilePluginText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - weight: 10
+`
+
+//nolint:dupword
+const errorBadProfilePluginRefText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: plover
+`
+
+//nolint:dupword
+const errorBadProfilePluginNameText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: plover
+`
+
+//nolint:dupword
+const errorBadPluginReferenceParametersText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: asdf
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: test1
+`
+
+//nolint:dupword
+const errorDuplicatePluginText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 20
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: test1
+`
+
+//nolint:dupword
+const errorDuplicateProfileText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: test2
+  pluginName: test-one
+  parameters:
+    threshold: 20
+- name: profileHandler
+  pluginName: test-profile-handler
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: test1
+- name: default
+  plugins:
+  - pluginRef: test2
+`
+
+// compile-time type validation
+var _ framework.Filter = &test1{}
+
+type test1 struct {
+	Threshold int `json:"threshold"`
+}
+
+func (f *test1) Name() string {
+	return test1Name
+}
+
+// Filter filters out pods that doesn't meet the filter criteria.
+func (f *test1) Filter(ctx context.Context, request *types.LLMRequest, cycleState *types.CycleState, pods []types.Pod) []types.Pod {
+	return pods
+}
+
+// compile-time type validation
+var _ framework.Scorer = &test2{}
+var _ framework.PostCycle = &test2{}
+
+type test2 struct{}
+
+func (f *test2) Name() string {
+	return test2Name
+}
+
+func (m *test2) Score(ctx context.Context, request *types.LLMRequest, cycleState *types.CycleState, pods []types.Pod) map[types.Pod]float64 {
+	return map[types.Pod]float64{}
+}
+
+func (m *test2) PostCycle(ctx context.Context, cycleState *types.CycleState, res *types.ProfileRunResult) {
+}
+
+// compile-time type validation
+var _ framework.Picker = &testPicker{}
+
+type testPicker struct{}
+
+func (p *testPicker) Name() string {
+	return testPickerName
+}
+
+func (p *testPicker) Pick(ctx context.Context, cycleState *types.CycleState, scoredPods []*types.ScoredPod) *types.ProfileRunResult {
+	return nil
+}
+
+// compile-time type validation
+var _ framework.ProfileHandler = &testProfileHandler{}
+
+type testProfileHandler struct{}
+
+func (p *testProfileHandler) Name() string {
+	return testProfileHandlerName
+}
+
+func (p *testProfileHandler) Pick(ctx context.Context, request *types.LLMRequest, profiles map[string]*framework.SchedulerProfile, executionResults map[string]*types.ProfileRunResult) map[string]*framework.SchedulerProfile {
+	return nil
+}
+
+func (p *testProfileHandler) ProcessResults(ctx context.Context, request *types.LLMRequest, profileResults map[string]*types.ProfileRunResult) *types.SchedulingResult {
+	return nil
+}
+
+func registerTestPlugins() {
+	plugins.Register(test1Name,
+		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
+			result := test1{}
+			err := json.Unmarshal(parameters, &result)
+			return &result, err
+		},
+	)
+
+	plugins.Register(test2Name,
+		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
+			return &test2{}, nil
+		},
+	)
+
+	plugins.Register(testPickerName,
+		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
+			return &testPicker{}, nil
+		},
+	)
+
+	plugins.Register(testProfileHandlerName,
+		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
+			return &testProfileHandler{}, nil
+		},
+	)
+}
diff --git a/pkg/epp/scheduling/scheduler_config_test.go b/pkg/epp/scheduling/scheduler_config_test.go
new file mode 100644
index 0000000..495df31
--- /dev/null
+++ b/pkg/epp/scheduling/scheduler_config_test.go
@@ -0,0 +1,263 @@
+/*
+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 (
+	"testing"
+
+	commonconfig "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/common/config"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+	"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/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"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+func TestLoadSchedulerConfig(t *testing.T) {
+	log := logutil.NewTestLogger()
+
+	tests := []struct {
+		name       string
+		configText string
+		wantErr    bool
+	}{
+		{
+			name:       "success",
+			configText: successConfigText,
+			wantErr:    false,
+		},
+		{
+			name:       "errorBadPluginJson",
+			configText: errorBadPluginJsonText,
+			wantErr:    true,
+		},
+		{
+			name:       "errorBadReferenceNoWeight",
+			configText: errorBadReferenceNoWeightText,
+			wantErr:    true,
+		},
+		{
+			name:       "errorPluginReferenceJson",
+			configText: errorPluginReferenceJsonText,
+			wantErr:    true,
+		},
+		{
+			name:       "errorTwoPickers",
+			configText: errorTwoPickersText,
+			wantErr:    true,
+		},
+		{
+			name:       "errorConfig",
+			configText: errorConfigText,
+			wantErr:    true,
+		},
+		{
+			name:       "errorTwoProfileHandlers",
+			configText: errorTwoProfileHandlersText,
+			wantErr:    true,
+		},
+		{
+			name:       "errorNoProfileHandlers",
+			configText: errorNoProfileHandlersText,
+			wantErr:    true,
+		},
+	}
+
+	registerNeededPlgugins()
+
+	for _, test := range tests {
+		theConfig, err := commonconfig.LoadConfig([]byte(test.configText), "")
+		if err != nil {
+			if test.wantErr {
+				continue
+			}
+			t.Fatalf("LoadConfig returned unexpected error: %v", err)
+		}
+		instantiatedPlugins, err := commonconfig.LoadPluginReferences(theConfig.Plugins, testHandle{})
+		if err != nil {
+			if test.wantErr {
+				continue
+			}
+			t.Fatalf("LoadPluginReferences returned unexpected error: %v", err)
+		}
+
+		_, err = LoadSchedulerConfig(theConfig.SchedulingProfiles, instantiatedPlugins, log)
+		if err != nil {
+			if !test.wantErr {
+				t.Errorf("LoadSchedulerConfig returned an unexpected error. error %v", err)
+			}
+		} else if test.wantErr {
+			t.Errorf("LoadSchedulerConfig did not return an expected error (%s)", test.name)
+		}
+	}
+}
+
+type testHandle struct {
+}
+
+func registerNeededPlgugins() {
+	allPlugins := map[string]plugins.Factory{
+		filter.LowQueueFilterName:        filter.LowQueueFilterFactory,
+		prefix.PrefixCachePluginName:     prefix.PrefixCachePluginFactory,
+		picker.MaxScorePickerName:        picker.MaxScorePickerFactory,
+		picker.RandomPickerName:          picker.RandomPickerFactory,
+		profile.SingleProfileHandlerName: profile.SingleProfileHandlerFactory,
+	}
+	for name, factory := range allPlugins {
+		plugins.Register(name, factory)
+	}
+}
+
+// The following multi-line string constants, cause false positive lint errors (dupword)
+
+//nolint:dupword
+const successConfigText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: lowQueue
+  pluginName: low-queue
+  parameters:
+    threshold: 10
+- name: prefixCache
+  pluginName: prefix-cache
+  parameters:
+    hashBlockSize: 32
+- name: maxScore
+  pluginName: max-score
+- name: profileHandler
+  pluginName: single-profile
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: lowQueue
+  - pluginRef: prefixCache
+    weight: 50
+  - pluginRef: maxScore
+`
+
+//nolint:dupword
+const errorBadPluginJsonText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name:profileHandler
+  pluginName: single-profile
+- name: prefixCache
+  pluginName: prefix-cache
+  parameters:
+    hashBlockSize: asdf
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: prefixCache
+    weight: 50
+`
+
+//nolint:dupword
+const errorBadReferenceNoWeightText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: profileHandler
+  pluginName: single-profile
+- name: prefixCache
+  pluginName: prefix-cache
+  parameters:
+    hashBlockSize: 32
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: prefixCache
+`
+
+//nolint:dupword
+const errorPluginReferenceJsonText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: lowQueue
+  pluginName: low-queue
+  parameters:
+    threshold: qwer
+- name: profileHandler
+  pluginName: single-profile
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: lowQueue
+`
+
+//nolint:dupword
+const errorTwoPickersText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: profileHandler
+  pluginName: single-profile
+- name: maxScore
+  pluginName: max-score
+- name: random
+  pluginName: random
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: maxScore
+  - pluginRef: random
+`
+
+//nolint:dupword
+const errorConfigText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: lowQueue
+  pluginName: low-queue
+  parameters:
+    threshold: 10
+`
+
+//nolint:dupword
+const errorTwoProfileHandlersText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: profileHandler
+  pluginName: single-profile
+- name: secondProfileHandler
+  pluginName: single-profile
+- name: maxScore
+  pluginName: max-score
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: maxScore
+`
+
+//nolint:dupword
+const errorNoProfileHandlersText = `
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: maxScore
+  pluginName: max-score
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: maxScore
+`
diff --git a/test/testdata/configloader_1_test.yaml b/test/testdata/configloader_1_test.yaml
new file mode 100644
index 0000000..bd66b45
--- /dev/null
+++ b/test/testdata/configloader_1_test.yaml
@@ -0,0 +1,22 @@
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- name: test1
+  pluginName: test-one
+  parameters:
+    threshold: 10
+- name: profileHandler
+  pluginName: test-profile-handler
+- pluginName: test-two
+  parameters:
+    hashBlockSize: 32
+- name: testPicker
+  pluginName: test-picker
+
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: test1
+  - pluginRef: test-two
+    weight: 50
+  - pluginRef: testPicker