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

Issue:
Expose baseline algorithm parameters as configurable
## Load Thresholds
* KVCacheThreshold
* QueueLengthThreshold

## metricsEndpoint
Currently ext proc is hard-coded to scrape from `[POD_IP]:[PORT]/metrics` for prometheus metrics. 
Different model server may expose different endpoints for scraping metrics. We'd like to introduce config fields to capture this information.

Hints from issue comments:
@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@kaushikmitr: @liu-cong we still need this @k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@nayihz: /remove-lifecycle rotten@nayihz: IIUC, we can add `MetricPort` and `MetricURLPath` field in `InferenceModelSpec`, then use it in `FetchMetrics` func. WDYT?

Solution patch:
diff --git a/Makefile b/Makefile
index fbb7300..dee7e99 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,9 @@ PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
 # The path to the E2E manifest file. It can be overridden by setting the
 # E2E_MANIFEST_PATH environment variable. Note that HF_TOKEN must be set when using the GPU-based manifest.
 E2E_MANIFEST_PATH ?= config/manifests/vllm/sim-deployment.yaml
+# E2E_IMAGE specifies the image to be used when running e2e tests using make test-e2e.
+# it defaults to current image tag, but can be overwritten to test specific tags, releases, etc.
+E2E_IMAGE ?= $(IMAGE_TAG)
 # E2E_USE_KIND is a flag used in test-e2e target. when set to true it will load the e2e image into the kind cluster.
 # it is possible though to run e2e tests against clusters other than kind. in such a case, it is the user's responsibility to load
 # the image into the cluster.
@@ -130,19 +133,19 @@ vet: ## Run go vet against code.
 
 .PHONY: test
 test: manifests generate fmt vet envtest image-build ## Run tests.
-	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e | grep -v /conformance) -race -coverprofile cover.out
+	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e | grep -v /conformance) -race -coverprofile cover.out
 
 .PHONY: test-unit
 test-unit: ## Run unit tests.
-	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./pkg/... -race -coverprofile cover.out
+	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./pkg/... -race -coverprofile cover.out
 
 .PHONY: test-integration
-test-integration: ## Run integration tests.
-	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/integration/epp/... -race -coverprofile cover.out
+test-integration: envtest ## Run integration tests.
+	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/integration/epp/... -race -coverprofile cover.out
 
 .PHONY: test-e2e
 test-e2e: ## Run end-to-end tests against an existing Kubernetes cluster.
-	MANIFEST_PATH=$(PROJECT_DIR)/$(E2E_MANIFEST_PATH) E2E_IMAGE=$(IMAGE_TAG) USE_KIND=$(E2E_USE_KIND) ./hack/test-e2e.sh
+	MANIFEST_PATH=$(PROJECT_DIR)/$(E2E_MANIFEST_PATH) E2E_IMAGE=$(E2E_IMAGE) USE_KIND=$(E2E_USE_KIND) ./hack/test-e2e.sh
 
 .PHONY: lint
 lint: golangci-lint ## Run golangci-lint linter
diff --git a/api/v1alpha2/inferencepool_types.go b/api/v1alpha2/inferencepool_types.go
index 214e36d..90228f6 100644
--- a/api/v1alpha2/inferencepool_types.go
+++ b/api/v1alpha2/inferencepool_types.go
@@ -95,9 +95,14 @@ type Extension struct {
 
 // ExtensionReference is a reference to the extension.
 //
+// Connections to this extension MUST use TLS by default. Implementations MAY
+// provide a way to customize this connection to use cleartext, a different
+// protocol, or custom TLS configuration.
+//
 // If a reference is invalid, the implementation MUST update the `ResolvedRefs`
-// Condition on the InferencePool's status to `status: False`. A 5XX status code MUST be returned
-// for the request that would have otherwise been routed to the invalid backend.
+// Condition on the InferencePool's status to `status: False`. A 5XX status code
+// MUST be returned for the request that would have otherwise been routed to the
+// invalid backend.
 type ExtensionReference struct {
 	// Group is the group of the referent.
 	// The default value is "", representing the Core API group.
diff --git a/cmd/epp/runner/register.go b/cmd/epp/runner/register.go
index 3a741d5..ce0bc35 100644
--- a/cmd/epp/runner/register.go
+++ b/cmd/epp/runner/register.go
@@ -44,8 +44,8 @@ func RegisterAllPlugins() {
 
 // eppHandle is an implementation of the interface plugins.Handle
 type eppHandle struct {
-	ctx     context.Context
-	plugins plugins.HandlePlugins
+	ctx context.Context
+	plugins.HandlePlugins
 }
 
 // Context returns a context the plugins can use, if they need one
@@ -53,30 +53,25 @@ func (h *eppHandle) Context() context.Context {
 	return h.ctx
 }
 
-// Plugins returns the sub-handle for working with instantiated plugins
-func (h *eppHandle) Plugins() plugins.HandlePlugins {
-	return h.plugins
-}
-
 // eppHandlePlugins implements the set of APIs to work with instantiated plugins
 type eppHandlePlugins struct {
-	thePlugins map[string]plugins.Plugin
+	plugins map[string]plugins.Plugin
 }
 
 // Plugin returns the named plugin instance
 func (h *eppHandlePlugins) Plugin(name string) plugins.Plugin {
-	return h.thePlugins[name]
+	return h.plugins[name]
 }
 
 // AddPlugin adds a plugin to the set of known plugin instances
 func (h *eppHandlePlugins) AddPlugin(name string, plugin plugins.Plugin) {
-	h.thePlugins[name] = plugin
+	h.plugins[name] = plugin
 }
 
 // GetAllPlugins returns all of the known plugins
 func (h *eppHandlePlugins) GetAllPlugins() []plugins.Plugin {
 	result := make([]plugins.Plugin, 0)
-	for _, plugin := range h.thePlugins {
+	for _, plugin := range h.plugins {
 		result = append(result, plugin)
 	}
 	return result
@@ -84,14 +79,14 @@ func (h *eppHandlePlugins) GetAllPlugins() []plugins.Plugin {
 
 // GetAllPluginsWithNames returns al of the known plugins with their names
 func (h *eppHandlePlugins) GetAllPluginsWithNames() map[string]plugins.Plugin {
-	return h.thePlugins
+	return h.plugins
 }
 
 func newEppHandle(ctx context.Context) *eppHandle {
 	return &eppHandle{
 		ctx: ctx,
-		plugins: &eppHandlePlugins{
-			thePlugins: map[string]plugins.Plugin{},
+		HandlePlugins: &eppHandlePlugins{
+			plugins: map[string]plugins.Plugin{},
 		},
 	}
 }
diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go
index 1340ff6..730ea22 100644
--- a/cmd/epp/runner/runner.go
+++ b/cmd/epp/runner/runner.go
@@ -21,6 +21,7 @@ import (
 	"flag"
 	"fmt"
 	"net/http/pprof"
+	"os"
 
 	"github.com/go-logr/logr"
 	"github.com/prometheus/client_golang/prometheus"
@@ -47,7 +48,6 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/saturationdetector"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/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"
@@ -64,10 +64,12 @@ var (
 		"The gRPC port used for communicating with Envoy proxy")
 	grpcHealthPort = flag.Int(
 		"grpcHealthPort",
-		9003,
+		runserver.DefaultGrpcHealthPort,
 		"The port used for gRPC liveness and readiness probes")
 	metricsPort = flag.Int(
-		"metricsPort", 9090, "The metrics port")
+		"metricsPort",
+		runserver.DefaultMetricsPort,
+		"The metrics port")
 	destinationEndpointHintKey = flag.String(
 		"destinationEndpointHintKey",
 		runserver.DefaultDestinationEndpointHintKey,
@@ -93,28 +95,51 @@ var (
 		"refreshPrometheusMetricsInterval",
 		runserver.DefaultRefreshPrometheusMetricsInterval,
 		"interval to flush prometheus metrics")
-	logVerbosity  = flag.Int("v", logging.DEFAULT, "number for the log level verbosity")
+	logVerbosity = flag.Int(
+		"v",
+		logging.DEFAULT,
+		"number for the log level verbosity")
 	secureServing = flag.Bool(
-		"secureServing", runserver.DefaultSecureServing, "Enables secure serving. Defaults to true.")
-	healthChecking = flag.Bool("healthChecking", runserver.DefaultHealthChecking, "Enables health checking")
-	certPath       = flag.String(
-		"certPath", "", "The path to the certificate for secure serving. The certificate and private key files "+
+		"secureServing",
+		runserver.DefaultSecureServing,
+		"Enables secure serving. Defaults to true.")
+	healthChecking = flag.Bool(
+		"healthChecking",
+		runserver.DefaultHealthChecking,
+		"Enables health checking")
+	certPath = flag.String(
+		"certPath",
+		runserver.DefaultCertPath,
+		"The path to the certificate for secure serving. The certificate and private key files "+
 			"are assumed to be named tls.crt and tls.key, respectively. If not set, and secureServing is enabled, "+
 			"then a self-signed certificate is used.")
 	// metric flags
-	totalQueuedRequestsMetric = flag.String("totalQueuedRequestsMetric",
-		"vllm:num_requests_waiting",
+	totalQueuedRequestsMetric = flag.String(
+		"totalQueuedRequestsMetric",
+		runserver.DefaultTotalQueuedRequestsMetric,
 		"Prometheus metric for the number of queued requests.")
-	kvCacheUsagePercentageMetric = flag.String("kvCacheUsagePercentageMetric",
-		"vllm:gpu_cache_usage_perc",
+	kvCacheUsagePercentageMetric = flag.String(
+		"kvCacheUsagePercentageMetric",
+		runserver.DefaultKvCacheUsagePercentageMetric,
 		"Prometheus metric for the fraction of KV-cache blocks currently in use (from 0 to 1).")
 	// LoRA metrics
-	loraInfoMetric = flag.String("loraInfoMetric",
-		"vllm:lora_requests_info",
+	loraInfoMetric = flag.String(
+		"loraInfoMetric",
+		runserver.DefaultLoraInfoMetric,
 		"Prometheus metric for the LoRA info metrics (must be in vLLM label format).")
 	// configuration flags
-	configFile = flag.String("configFile", "", "The path to the configuration file")
-	configText = flag.String("configText", "", "The configuration specified as text, in lieu of a file")
+	configFile = flag.String(
+		"configFile",
+		runserver.DefaultConfigFile,
+		"The path to the configuration file")
+	configText = flag.String(
+		"configText",
+		runserver.DefaultConfigText,
+		"The configuration specified as text, in lieu of a file")
+
+	modelServerMetricsPort = flag.Int("modelServerMetricsPort", 0, "Port to scrape metrics from pods. "+
+		"Default value will be set to InferencePool.Spec.TargetPortNumber if not set.")
+	modelServerMetricsPath = flag.String("modelServerMetricsPath", "/metrics", "Path to scrape metrics from pods")
 
 	setupLog = ctrl.Log.WithName("setup")
 
@@ -147,7 +172,32 @@ func (r *Runner) WithSchedulerConfig(schedulerConfig *scheduling.SchedulerConfig
 	return r
 }
 
+func bindEnvToFlags() {
+	// map[ENV_VAR]flagName   – add more as needed
+	for env, flg := range map[string]string{
+		"GRPC_PORT":                     "grpcPort",
+		"GRPC_HEALTH_PORT":              "grpcHealthPort",
+		"MODEL_SERVER_METRICS_PORT":     "modelServerMetricsPort",
+		"MODEL_SERVER_METRICS_PATH":     "modelServerMetricsPath",
+		"DESTINATION_ENDPOINT_HINT_KEY": "destinationEndpointHintKey",
+		"POOL_NAME":                     "poolName",
+		"POOL_NAMESPACE":                "poolNamespace",
+		// durations & bools work too; flag.Set expects the *string* form
+		"REFRESH_METRICS_INTERVAL": "refreshMetricsInterval",
+		"SECURE_SERVING":           "secureServing",
+	} {
+		if v := os.Getenv(env); v != "" {
+			// ignore error; Parse() will catch invalid values later
+			_ = flag.Set(flg, v)
+		}
+	}
+}
+
 func (r *Runner) Run(ctx context.Context) error {
+	// Defaults already baked into flag declarations
+	// Load env vars as "soft" overrides
+	bindEnvToFlags()
+
 	opts := zap.Options{
 		Development: true,
 	}
@@ -189,7 +239,11 @@ func (r *Runner) Run(ctx context.Context) error {
 		return err
 	}
 	verifyMetricMapping(*mapping, setupLog)
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.PodMetricsClientImpl{MetricMapping: mapping}, *refreshMetricsInterval)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.PodMetricsClientImpl{
+		MetricMapping:          mapping,
+		ModelServerMetricsPort: int32(*modelServerMetricsPort),
+		ModelServerMetricsPath: *modelServerMetricsPath,
+	}, *refreshMetricsInterval)
 
 	datastore := datastore.NewDatastore(ctx, pmf)
 
@@ -292,10 +346,9 @@ func (r *Runner) initializeScheduler() (*scheduling.Scheduler, error) {
 		kvCacheScorerWeight := envutil.GetEnvInt("KV_CACHE_SCORE_WEIGHT", scorer.DefaultKVCacheScorerWeight, setupLog)
 
 		schedulerProfile := framework.NewSchedulerProfile().
-			WithFilters(filter.NewSubsetFilter()).
 			WithScorers(framework.NewWeightedScorer(scorer.NewQueueScorer(), queueScorerWeight),
 				framework.NewWeightedScorer(scorer.NewKVCacheScorer(), kvCacheScorerWeight)).
-			WithPicker(picker.NewMaxScorePicker())
+			WithPicker(picker.NewMaxScorePicker(picker.DefaultMaxNumOfEndpoints))
 
 		if prefixCacheScheduling {
 			prefixScorerWeight := envutil.GetEnvInt("PREFIX_CACHE_SCORE_WEIGHT", prefix.DefaultScorerWeight, setupLog)
@@ -316,27 +369,35 @@ func (r *Runner) initializeScheduler() (*scheduling.Scheduler, error) {
 }
 
 func (r *Runner) parseConfiguration(ctx context.Context) error {
-	if len(*configText) != 0 || len(*configFile) != 0 {
-		theConfig, err := loader.LoadConfig([]byte(*configText), *configFile)
-		if err != nil {
-			return fmt.Errorf("failed to load the configuration - %w", err)
-		}
-
-		epp := newEppHandle(ctx)
+	if *configText == "" && *configFile == "" {
+		return nil // configuring through code, not through file
+	}
 
-		err = loader.LoadPluginReferences(theConfig.Plugins, epp)
+	var configBytes []byte
+	if *configText != "" {
+		configBytes = []byte(*configText)
+	} else if *configFile != "" { // if config was specified through a file
+		var err error
+		configBytes, err = os.ReadFile(*configFile)
 		if err != nil {
-			return fmt.Errorf("failed to instantiate the plugins - %w", err)
+			return fmt.Errorf("failed to load config from a file '%s' - %w", *configFile, err)
 		}
+	}
 
-		r.schedulerConfig, err = loader.LoadSchedulerConfig(theConfig.SchedulingProfiles, epp)
-		if err != nil {
-			return fmt.Errorf("failed to create Scheduler configuration - %w", err)
-		}
+	handle := newEppHandle(ctx)
+	config, err := loader.LoadConfig(configBytes, handle)
+	if err != nil {
+		return fmt.Errorf("failed to load the configuration - %w", err)
+	}
 
-		// Add requestControl plugins
-		r.requestControlConfig.AddPlugins(epp.Plugins().GetAllPlugins()...)
+	r.schedulerConfig, err = loader.LoadSchedulerConfig(config.SchedulingProfiles, handle)
+	if err != nil {
+		return fmt.Errorf("failed to create Scheduler configuration - %w", err)
 	}
+
+	// Add requestControl plugins
+	r.requestControlConfig.AddPlugins(handle.GetAllPlugins()...)
+
 	return nil
 }
 
@@ -397,8 +458,8 @@ func validateFlags() error {
 	if *poolName == "" {
 		return fmt.Errorf("required %q flag not set", "poolName")
 	}
-	if len(*configText) != 0 && len(*configFile) != 0 {
-		return fmt.Errorf("both the %s and %s flags can not be set at the same time", "configText", "configFile")
+	if *configText != "" && *configFile != "" {
+		return fmt.Errorf("both the %q and %q flags can not be set at the same time", "configText", "configFile")
 	}
 
 	return nil
diff --git a/config/manifests/vllm/sim-deployment.yaml b/config/manifests/vllm/sim-deployment.yaml
index 1862efe..196fe86 100644
--- a/config/manifests/vllm/sim-deployment.yaml
+++ b/config/manifests/vllm/sim-deployment.yaml
@@ -14,7 +14,7 @@ spec:
     spec:
       containers:
       - name: vllm-sim
-        image: ghcr.io/llm-d/llm-d-inference-sim:latest
+        image: ghcr.io/llm-d/llm-d-inference-sim:v0.1.2
         imagePullPolicy: Always
         args:
         - --model
diff --git a/conformance/utils/config/timing.go b/conformance/utils/config/timing.go
index 861eb0f..00aefda 100644
--- a/conformance/utils/config/timing.go
+++ b/conformance/utils/config/timing.go
@@ -47,6 +47,7 @@ type InferenceExtensionTimeoutConfig struct {
 func DefaultInferenceExtensionTimeoutConfig() InferenceExtensionTimeoutConfig {
 	config := gatewayconfig.DefaultTimeoutConfig()
 	config.HTTPRouteMustHaveCondition = 300 * time.Second
+	config.RouteMustHaveParents = 200 * time.Second
 	config.MaxTimeToConsistency = 200 * time.Second
 	config.DefaultTestTimeout = 600 * time.Second
 	return InferenceExtensionTimeoutConfig{
diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go
index f32bafb..f2e1e21 100644
--- a/conformance/utils/kubernetes/helpers.go
+++ b/conformance/utils/kubernetes/helpers.go
@@ -69,7 +69,7 @@ func checkCondition(t *testing.T, conditions []metav1.Condition, expectedConditi
 // InferencePoolMustHaveCondition waits for the specified InferencePool resource
 // to exist and report the expected status condition within one of its parent statuses.
 // It polls the InferencePool's status until the condition is met or the timeout occurs.
-func InferencePoolMustHaveCondition(t *testing.T, c client.Client, poolNN types.NamespacedName, expectedCondition metav1.Condition) {
+func InferencePoolMustHaveCondition(t *testing.T, c client.Reader, poolNN types.NamespacedName, expectedCondition metav1.Condition) {
 	t.Helper() // Marks this function as a test helper
 
 	var timeoutConfig config.InferenceExtensionTimeoutConfig = config.DefaultInferenceExtensionTimeoutConfig()
@@ -159,7 +159,7 @@ func InferencePoolMustHaveCondition(t *testing.T, c client.Client, poolNN types.
 // InferencePoolMustHaveNoParents waits for the specified InferencePool resource
 // to exist and report that it has no parent references in its status.
 // This typically indicates it is no longer referenced by any Gateway API resources.
-func InferencePoolMustHaveNoParents(t *testing.T, c client.Client, poolNN types.NamespacedName) {
+func InferencePoolMustHaveNoParents(t *testing.T, c client.Reader, poolNN types.NamespacedName) {
 	t.Helper()
 
 	var lastObservedPool *inferenceapi.InferencePool
@@ -242,7 +242,7 @@ func HTTPRouteMustBeAcceptedAndResolved(t *testing.T, c client.Client, timeoutCo
 // InferencePoolMustBeAcceptedByParent waits for the specified InferencePool
 // to report an Accepted condition with status True and reason "Accepted"
 // from at least one of its parent Gateways.
-func InferencePoolMustBeAcceptedByParent(t *testing.T, c client.Client, poolNN types.NamespacedName) {
+func InferencePoolMustBeAcceptedByParent(t *testing.T, c client.Reader, poolNN types.NamespacedName) {
 	t.Helper()
 
 	acceptedByParentCondition := metav1.Condition{
@@ -259,7 +259,7 @@ func InferencePoolMustBeAcceptedByParent(t *testing.T, c client.Client, poolNN t
 // InferencePoolMustBeRouteAccepted waits for the specified InferencePool resource
 // to exist and report an Accepted condition with Type=RouteConditionAccepted,
 // Status=True, and Reason=RouteReasonAccepted within one of its parent statuses.
-func InferencePoolMustBeRouteAccepted(t *testing.T, c client.Client, poolNN types.NamespacedName) {
+func InferencePoolMustBeRouteAccepted(t *testing.T, c client.Reader, poolNN types.NamespacedName) {
 	t.Helper()
 
 	expectedPoolCondition := metav1.Condition{
@@ -310,7 +310,7 @@ func GetGatewayEndpoint(t *testing.T, k8sClient client.Client, timeoutConfig gat
 
 // GetPodsWithLabel retrieves a list of Pods.
 // It finds pods matching the given labels in a specific namespace.
-func GetPodsWithLabel(t *testing.T, c client.Client, namespace string, labels map[string]string, timeConfig gatewayapiconfig.TimeoutConfig) ([]corev1.Pod, error) {
+func GetPodsWithLabel(t *testing.T, c client.Reader, namespace string, labels map[string]string, timeConfig gatewayapiconfig.TimeoutConfig) ([]corev1.Pod, error) {
 	t.Helper()
 
 	pods := &corev1.PodList{}
diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go
index b658639..f53cc32 100644
--- a/conformance/utils/traffic/traffic.go
+++ b/conformance/utils/traffic/traffic.go
@@ -159,7 +159,7 @@ func waitForConvergeToExpected(
 			return false
 		}
 
-		if err := gwhttp.CompareRequest(t, &request.Request, cReq, cRes, expectedResponse); err != nil {
+		if err := CompareRequestWithWildcardStatus(t, &request.Request, cReq, cRes, expectedResponse); err != nil {
 			tlog.Logf(t, "Response expectation failed for request: %+v  not ready yet: %v (after %v)", request.Request, err, elapsed)
 			return false
 		}
@@ -169,6 +169,26 @@ func waitForConvergeToExpected(
 	tlog.Logf(t, "Request passed")
 }
 
+// CompareRequestWithWildcardStatus compares requests with wildcard status code support.
+// It treats a single-digit expected code (e.g., 4) as a class wildcard (4xx),
+// while standard 3-digit codes are matched exactly.
+func CompareRequestWithWildcardStatus(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected gwhttp.ExpectedResponse) error {
+	if expected.Response.StatusCode < 1 || expected.Response.StatusCode >= 100 {
+		return gwhttp.CompareRequest(t, req, cReq, cRes, expected)
+	}
+
+	expectedClass := expected.Response.StatusCode
+	actualClass := cRes.StatusCode / 100
+	if expectedClass != actualClass {
+		return fmt.Errorf("expected status code class %dxx, but got %d", expectedClass, cRes.StatusCode)
+	}
+
+	// StatusCode Class matches; update status code on a copy to allow the standard comparator to pass.
+	modifiedExpected := expected
+	modifiedExpected.Response.StatusCode = cRes.StatusCode
+	return gwhttp.CompareRequest(t, req, cReq, cRes, modifiedExpected)
+}
+
 // TODO: https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031
 // remove this when sigs.k8s.io/gateway-api/conformance/utils/roundtripper is able to send request with body.
 // RequestWithBody extends roundtripper.Request to include a request body.
diff --git a/go.mod b/go.mod
index 5e04947..a0cc2e1 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/sync v0.14.0
+	golang.org/x/sync v0.15.0
 	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 	k8s.io/api v0.33.2
@@ -31,7 +31,7 @@ require (
 	sigs.k8s.io/controller-runtime v0.21.0
 	sigs.k8s.io/gateway-api v1.3.0
 	sigs.k8s.io/structured-merge-diff/v4 v4.7.0
-	sigs.k8s.io/yaml v1.4.0
+	sigs.k8s.io/yaml v1.5.0
 )
 
 require (
@@ -104,6 +104,7 @@ require (
 	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.4.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/crypto v0.38.0 // indirect
 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
 	golang.org/x/mod v0.24.0 // indirect
diff --git a/go.sum b/go.sum
index d4b03f7..9a74921 100644
--- a/go.sum
+++ b/go.sum
@@ -219,6 +219,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -241,8 +245,8 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -332,5 +336,6 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
 sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
 sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
 sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/mkdocs.yml b/mkdocs.yml
index 54984db..d22d937 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -71,7 +71,6 @@ nav:
       - Metrics and Observability: guides/metrics-and-observability.md
       - 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
diff --git a/pkg/bbr/handlers/server.go b/pkg/bbr/handlers/server.go
index eb6b93d..a580380 100644
--- a/pkg/bbr/handlers/server.go
+++ b/pkg/bbr/handlers/server.go
@@ -118,7 +118,7 @@ type streamedBody struct {
 func (s *Server) processRequestBody(ctx context.Context, body *extProcPb.HttpBody, streamedBody *streamedBody, logger logr.Logger) ([]*extProcPb.ProcessingResponse, error) {
 	loggerVerbose := logger.V(logutil.VERBOSE)
 
-	var requestBody map[string]interface{}
+	var requestBody map[string]any
 	if s.streaming {
 		streamedBody.body = append(streamedBody.body, body.Body...)
 		// In the stream case, we can receive multiple request bodies.
diff --git a/pkg/epp/backend/metrics/fake.go b/pkg/epp/backend/metrics/fake.go
index 5599d4e..6828318 100644
--- a/pkg/epp/backend/metrics/fake.go
+++ b/pkg/epp/backend/metrics/fake.go
@@ -56,7 +56,7 @@ type FakePodMetricsClient struct {
 	Res   map[types.NamespacedName]*MetricsState
 }
 
-func (f *FakePodMetricsClient) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *MetricsState, port int32) (*MetricsState, error) {
+func (f *FakePodMetricsClient) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *MetricsState, _ int32) (*MetricsState, error) {
 	f.errMu.RLock()
 	err, ok := f.Err[pod.NamespacedName]
 	f.errMu.RUnlock()
diff --git a/pkg/epp/backend/metrics/logger.go b/pkg/epp/backend/metrics/logger.go
index 7dc1a8b..9494f14 100644
--- a/pkg/epp/backend/metrics/logger.go
+++ b/pkg/epp/backend/metrics/logger.go
@@ -111,5 +111,5 @@ func refreshPrometheusMetrics(logger logr.Logger, datastore Datastore) {
 	podTotalCount := len(podMetrics)
 	metrics.RecordInferencePoolAvgKVCache(pool.Name, kvCacheTotal/float64(podTotalCount))
 	metrics.RecordInferencePoolAvgQueueSize(pool.Name, float64(queueTotal/podTotalCount))
-	metrics.RecordinferencePoolReadyPods(pool.Name, float64(podTotalCount))
+	metrics.RecordInferencePoolReadyPods(pool.Name, float64(podTotalCount))
 }
diff --git a/pkg/epp/backend/metrics/metrics.go b/pkg/epp/backend/metrics/metrics.go
index 8899e00..590685c 100644
--- a/pkg/epp/backend/metrics/metrics.go
+++ b/pkg/epp/backend/metrics/metrics.go
@@ -37,15 +37,14 @@ const (
 )
 
 type PodMetricsClientImpl struct {
-	MetricMapping *MetricMapping
+	MetricMapping          *MetricMapping
+	ModelServerMetricsPort int32
+	ModelServerMetricsPath string
 }
 
 // FetchMetrics fetches metrics from a given pod, clones the existing metrics object and returns an updated one.
 func (p *PodMetricsClientImpl) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *MetricsState, port int32) (*MetricsState, error) {
-	// Currently the metrics endpoint is hard-coded, which works with vLLM.
-	// TODO(https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/16): Consume this from InferencePool config.
-	url := "http://" + pod.Address + ":" + strconv.Itoa(int(port)) + "/metrics"
-
+	url := p.getMetricEndpoint(pod, port)
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create request: %v", err)
@@ -70,6 +69,13 @@ func (p *PodMetricsClientImpl) FetchMetrics(ctx context.Context, pod *backend.Po
 	return p.promToPodMetrics(metricFamilies, existing)
 }
 
+func (p *PodMetricsClientImpl) getMetricEndpoint(pod *backend.Pod, targetPortNumber int32) string {
+	if p.ModelServerMetricsPort == 0 {
+		p.ModelServerMetricsPort = targetPortNumber
+	}
+	return fmt.Sprintf("http://%s:%d%s", pod.Address, p.ModelServerMetricsPort, p.ModelServerMetricsPath)
+}
+
 // promToPodMetrics updates internal pod metrics with scraped Prometheus metrics.
 func (p *PodMetricsClientImpl) promToPodMetrics(
 	metricFamilies map[string]*dto.MetricFamily,
diff --git a/pkg/epp/backend/metrics/metrics_state.go b/pkg/epp/backend/metrics/metrics_state.go
index 3be7d53..0215ac0 100644
--- a/pkg/epp/backend/metrics/metrics_state.go
+++ b/pkg/epp/backend/metrics/metrics_state.go
@@ -21,8 +21,8 @@ import (
 	"time"
 )
 
-// newMetricsState initializes a new MetricsState and returns its pointer.
-func newMetricsState() *MetricsState {
+// NewMetricsState initializes a new MetricsState and returns its pointer.
+func NewMetricsState() *MetricsState {
 	return &MetricsState{
 		ActiveModels:  make(map[string]int),
 		WaitingModels: make(map[string]int),
diff --git a/pkg/epp/backend/metrics/types.go b/pkg/epp/backend/metrics/types.go
index bb78c2b..80b7085 100644
--- a/pkg/epp/backend/metrics/types.go
+++ b/pkg/epp/backend/metrics/types.go
@@ -51,7 +51,7 @@ func (f *PodMetricsFactory) NewPodMetrics(parentCtx context.Context, in *corev1.
 		logger:    log.FromContext(parentCtx).WithValues("pod", pod.NamespacedName),
 	}
 	pm.pod.Store(pod)
-	pm.metrics.Store(newMetricsState())
+	pm.metrics.Store(NewMetricsState())
 
 	pm.startRefreshLoop(parentCtx)
 	return pm
diff --git a/pkg/epp/common/config/loader/configloader.go b/pkg/epp/common/config/loader/configloader.go
index 864a3ea..3effeff 100644
--- a/pkg/epp/common/config/loader/configloader.go
+++ b/pkg/epp/common/config/loader/configloader.go
@@ -19,11 +19,11 @@ package loader
 import (
 	"errors"
 	"fmt"
-	"os"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	"sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
 	configapi "sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
@@ -39,77 +39,54 @@ func init() {
 	utilruntime.Must(configapi.Install(scheme))
 }
 
-// Load config either from supplied text or from a file
-func LoadConfig(configText []byte, fileName string) (*configapi.EndpointPickerConfig, error) {
-	var err error
-	if len(configText) == 0 {
-		configText, err = os.ReadFile(fileName)
-		if err != nil {
-			return nil, fmt.Errorf("failed to load config file. Error: %s", err)
-		}
-	}
-
-	theConfig := &configapi.EndpointPickerConfig{}
+// Load config from supplied text that was converted to []byte
+func LoadConfig(configBytes []byte, handle plugins.Handle) (*configapi.EndpointPickerConfig, error) {
+	config := &configapi.EndpointPickerConfig{}
 
 	codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict)
-	err = runtime.DecodeInto(codecs.UniversalDecoder(), configText, theConfig)
+	err := runtime.DecodeInto(codecs.UniversalDecoder(), configBytes, config)
 	if err != nil {
-		return nil, fmt.Errorf("the configuration is invalid. Error: %s", err)
+		return nil, fmt.Errorf("the configuration is invalid - %w", err)
 	}
 
-	// Validate loaded configuration
-	err = validateConfiguration(theConfig)
-	if err != nil {
-		return nil, fmt.Errorf("the configuration is invalid. error: %s", err)
+	// instantiate loaded plugins
+	if err = instantiatePlugins(config.Plugins, handle); err != nil {
+		return nil, fmt.Errorf("failed to instantiate plugins - %w", err)
 	}
-	return theConfig, nil
-}
 
-func LoadPluginReferences(thePlugins []configapi.PluginSpec, handle plugins.Handle) error {
-	for _, pluginConfig := range thePlugins {
-		thePlugin, err := instantiatePlugin(pluginConfig, handle)
-		if err != nil {
-			return err
-		}
-		handle.Plugins().AddPlugin(pluginConfig.Name, thePlugin)
+	if err = validateSchedulingProfiles(config); err != nil {
+		return nil, fmt.Errorf("failed to validate scheduling profiles - %w", err)
 	}
-	return nil
+
+	return config, nil
 }
 
 func LoadSchedulerConfig(configProfiles []v1alpha1.SchedulingProfile, handle plugins.Handle) (*scheduling.SchedulerConfig, error) {
-
-	var profiles = map[string]*framework.SchedulerProfile{}
-
-	for _, configProfile := range configProfiles {
-		profile := framework.SchedulerProfile{}
-
-		for _, plugin := range configProfile.Plugins {
-			var err error
-			thePlugin := handle.Plugins().Plugin(plugin.PluginRef)
-			if theScorer, ok := thePlugin.(framework.Scorer); ok {
+	profiles := map[string]*framework.SchedulerProfile{}
+	for _, namedProfile := range configProfiles {
+		profile := framework.NewSchedulerProfile()
+		for _, plugin := range namedProfile.Plugins {
+			referencedPlugin := handle.Plugin(plugin.PluginRef)
+			if scorer, ok := referencedPlugin.(framework.Scorer); ok {
 				if plugin.Weight == nil {
 					return nil, fmt.Errorf("scorer '%s' is missing a weight", plugin.PluginRef)
 				}
-				thePlugin = framework.NewWeightedScorer(theScorer, *plugin.Weight)
+				referencedPlugin = framework.NewWeightedScorer(scorer, *plugin.Weight)
 			}
-			err = profile.AddPlugins(thePlugin)
-			if err != nil {
-				return nil, err
+			if err := profile.AddPlugins(referencedPlugin); err != nil {
+				return nil, fmt.Errorf("failed to load scheduler config - %w", err)
 			}
 		}
-		profiles[configProfile.Name] = &profile
+		profiles[namedProfile.Name] = profile
 	}
 
 	var profileHandler framework.ProfileHandler
-	var profileHandlerName string
-
-	for pluginName, thePlugin := range handle.Plugins().GetAllPluginsWithNames() {
-		if theProfileHandler, ok := thePlugin.(framework.ProfileHandler); ok {
+	for pluginName, plugin := range handle.GetAllPluginsWithNames() {
+		if theProfileHandler, ok := plugin.(framework.ProfileHandler); ok {
 			if profileHandler != nil {
-				return nil, fmt.Errorf("only one profile handler is allowed. Both %s and %s are profile handlers", profileHandlerName, pluginName)
+				return nil, fmt.Errorf("only one profile handler is allowed. Both %s and %s are profile handlers", profileHandler.TypedName().Name, pluginName)
 			}
 			profileHandler = theProfileHandler
-			profileHandlerName = pluginName
 		}
 	}
 	if profileHandler == nil {
@@ -119,62 +96,61 @@ func LoadSchedulerConfig(configProfiles []v1alpha1.SchedulingProfile, handle plu
 	return scheduling.NewSchedulerConfig(profileHandler, profiles), nil
 }
 
-func instantiatePlugin(pluginSpec configapi.PluginSpec, handle plugins.Handle) (plugins.Plugin, error) {
-	factory, ok := plugins.Registry[pluginSpec.Type]
-	if !ok {
-		return nil, fmt.Errorf("failed to instantiate the plugin. plugin type %s not found", pluginSpec.Type)
-	}
-	thePlugin, err := factory(pluginSpec.Name, pluginSpec.Parameters, handle)
-	if err != nil {
-		return nil, fmt.Errorf("failed to instantiate the plugin type %s. Error: %s", pluginSpec.Type, err)
-	}
-	return thePlugin, err
-}
-
-func validateConfiguration(theConfig *configapi.EndpointPickerConfig) error {
-	names := make(map[string]struct{})
+func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle plugins.Handle) error {
+	pluginNames := sets.New[string]() // set of plugin names, a name must be unique
 
-	for _, pluginConfig := range theConfig.Plugins {
+	for _, pluginConfig := range configuredPlugins {
 		if pluginConfig.Type == "" {
-			return fmt.Errorf("plugin definition for %s is missing a type", pluginConfig.Name)
+			return fmt.Errorf("plugin definition for '%s' is missing a type", pluginConfig.Name)
 		}
 
-		if _, ok := names[pluginConfig.Name]; ok {
-			return fmt.Errorf("plugin name %s used more than once", pluginConfig.Name)
+		if pluginNames.Has(pluginConfig.Name) {
+			return fmt.Errorf("plugin name '%s' used more than once", pluginConfig.Name)
 		}
-		names[pluginConfig.Name] = struct{}{}
+		pluginNames.Insert(pluginConfig.Name)
 
-		_, ok := plugins.Registry[pluginConfig.Type]
+		factory, ok := plugins.Registry[pluginConfig.Type]
 		if !ok {
-			return fmt.Errorf("plugin type %s is not found", pluginConfig.Type)
+			return fmt.Errorf("plugin type '%s' is not found in registry", pluginConfig.Type)
 		}
+
+		plugin, err := factory(pluginConfig.Name, pluginConfig.Parameters, handle)
+		if err != nil {
+			return fmt.Errorf("failed to instantiate the plugin type '%s' - %w", pluginConfig.Type, err)
+		}
+
+		handle.AddPlugin(pluginConfig.Name, plugin)
 	}
 
-	if len(theConfig.SchedulingProfiles) == 0 {
+	return nil
+}
+
+func validateSchedulingProfiles(config *configapi.EndpointPickerConfig) error {
+	if len(config.SchedulingProfiles) == 0 {
 		return errors.New("there must be at least one scheduling profile in the configuration")
 	}
 
-	names = map[string]struct{}{}
-	for _, profile := range theConfig.SchedulingProfiles {
+	profileNames := sets.New[string]()
+	for _, profile := range config.SchedulingProfiles {
 		if profile.Name == "" {
-			return errors.New("SchedulingProfiles need a name")
+			return errors.New("SchedulingProfile must have a name")
 		}
 
-		if _, ok := names[profile.Name]; ok {
-			return fmt.Errorf("the name %s has been specified for more than one SchedulingProfile", profile.Name)
+		if profileNames.Has(profile.Name) {
+			return fmt.Errorf("the name '%s' has been specified for more than one SchedulingProfile", profile.Name)
 		}
-		names[profile.Name] = struct{}{}
+		profileNames.Insert(profile.Name)
 
 		if len(profile.Plugins) == 0 {
-			return errors.New("SchedulingProfiles need at least one plugin")
+			return fmt.Errorf("SchedulingProfile '%s' must have at least one plugin", profile.Name)
 		}
 		for _, plugin := range profile.Plugins {
 			if len(plugin.PluginRef) == 0 {
-				return errors.New("SchedulingProfile's plugins need a plugin reference")
+				return fmt.Errorf("SchedulingProfile '%s' plugins must have a plugin reference", profile.Name)
 			}
 
 			notFound := true
-			for _, pluginConfig := range theConfig.Plugins {
+			for _, pluginConfig := range config.Plugins {
 				if plugin.PluginRef == pluginConfig.Name {
 					notFound = false
 					break
diff --git a/pkg/epp/controller/inferencemodel_reconciler.go b/pkg/epp/controller/inferencemodel_reconciler.go
index a7f365b..f1774ff 100644
--- a/pkg/epp/controller/inferencemodel_reconciler.go
+++ b/pkg/epp/controller/inferencemodel_reconciler.go
@@ -34,7 +34,7 @@ import (
 )
 
 type InferenceModelReconciler struct {
-	client.Client
+	client.Reader
 	Record             record.EventRecorder
 	Datastore          datastore.Datastore
 	PoolNamespacedName types.NamespacedName
@@ -88,7 +88,7 @@ func (c *InferenceModelReconciler) handleModelDeleted(ctx context.Context, req t
 	logger.Info("InferenceModel removed from datastore", "poolRef", existing.Spec.PoolRef, "modelName", existing.Spec.ModelName)
 
 	// TODO(#409): replace this backfill logic with one that is based on InferenceModel Ready conditions once those are set by an external controller.
-	updated, err := c.Datastore.ModelResync(ctx, c.Client, existing.Spec.ModelName)
+	updated, err := c.Datastore.ModelResync(ctx, c.Reader, existing.Spec.ModelName)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/epp/controller/inferencepool_reconciler.go b/pkg/epp/controller/inferencepool_reconciler.go
index fb7d772..5478170 100644
--- a/pkg/epp/controller/inferencepool_reconciler.go
+++ b/pkg/epp/controller/inferencepool_reconciler.go
@@ -33,7 +33,7 @@ import (
 // This implementation is just used for reading & maintaining data sync. The Gateway implementation
 // will have the proper controller that will create/manage objects on behalf of the server pool.
 type InferencePoolReconciler struct {
-	client.Client
+	client.Reader
 	Record    record.EventRecorder
 	Datastore datastore.Datastore
 }
@@ -60,7 +60,7 @@ func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		return ctrl.Result{}, nil
 	}
 	// update pool in datastore
-	if err := c.Datastore.PoolSet(ctx, c.Client, infPool); err != nil {
+	if err := c.Datastore.PoolSet(ctx, c.Reader, infPool); err != nil {
 		logger.Error(err, "Failed to update datastore")
 		return ctrl.Result{}, err
 	}
diff --git a/pkg/epp/controller/pod_reconciler.go b/pkg/epp/controller/pod_reconciler.go
index 5f1df10..bf8cb44 100644
--- a/pkg/epp/controller/pod_reconciler.go
+++ b/pkg/epp/controller/pod_reconciler.go
@@ -35,7 +35,7 @@ import (
 )
 
 type PodReconciler struct {
-	client.Client
+	client.Reader
 	Datastore datastore.Datastore
 	Record    record.EventRecorder
 }
diff --git a/pkg/epp/datastore/datastore.go b/pkg/epp/datastore/datastore.go
index 5ae4964..5243554 100644
--- a/pkg/epp/datastore/datastore.go
+++ b/pkg/epp/datastore/datastore.go
@@ -48,7 +48,7 @@ type Datastore interface {
 	// PoolSet sets the given pool in datastore. If the given pool has different label selector than the previous pool
 	// that was stored, the function triggers a resync of the pods to keep the datastore updated. If the given pool
 	// is nil, this call triggers the datastore.Clear() function.
-	PoolSet(ctx context.Context, client client.Client, pool *v1alpha2.InferencePool) error
+	PoolSet(ctx context.Context, reader client.Reader, pool *v1alpha2.InferencePool) error
 	PoolGet() (*v1alpha2.InferencePool, error)
 	PoolHasSynced() bool
 	PoolLabelsMatch(podLabels map[string]string) bool
@@ -57,7 +57,7 @@ type Datastore interface {
 	ModelSetIfOlder(infModel *v1alpha2.InferenceModel) bool
 	ModelGet(modelName string) *v1alpha2.InferenceModel
 	ModelDelete(namespacedName types.NamespacedName) *v1alpha2.InferenceModel
-	ModelResync(ctx context.Context, ctrlClient client.Client, modelName string) (bool, error)
+	ModelResync(ctx context.Context, reader client.Reader, modelName string) (bool, error)
 	ModelGetAll() []*v1alpha2.InferenceModel
 
 	// PodMetrics operations
@@ -110,7 +110,7 @@ func (ds *datastore) Clear() {
 }
 
 // /// InferencePool APIs ///
-func (ds *datastore) PoolSet(ctx context.Context, client client.Client, pool *v1alpha2.InferencePool) error {
+func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, pool *v1alpha2.InferencePool) error {
 	if pool == nil {
 		ds.Clear()
 		return nil
@@ -129,7 +129,7 @@ func (ds *datastore) PoolSet(ctx context.Context, client client.Client, pool *v1
 		// 2) If the selector on the pool was updated, then we will not get any pod events, and so we need
 		//    to resync the whole pool: remove pods in the store that don't match the new selector and add
 		//    the ones that may have existed already to the store.
-		if err := ds.podResyncAll(ctx, client); err != nil {
+		if err := ds.podResyncAll(ctx, reader); err != nil {
 			return fmt.Errorf("failed to update pods according to the pool selector - %w", err)
 		}
 	}
@@ -182,12 +182,12 @@ func (ds *datastore) ModelSetIfOlder(infModel *v1alpha2.InferenceModel) bool {
 	return true
 }
 
-func (ds *datastore) ModelResync(ctx context.Context, c client.Client, modelName string) (bool, error) {
+func (ds *datastore) ModelResync(ctx context.Context, reader client.Reader, modelName string) (bool, error) {
 	ds.poolAndModelsMu.Lock()
 	defer ds.poolAndModelsMu.Unlock()
 
 	var models v1alpha2.InferenceModelList
-	if err := c.List(ctx, &models, client.MatchingFields{ModelNameIndexKey: modelName}, client.InNamespace(ds.pool.Namespace)); err != nil {
+	if err := reader.List(ctx, &models, client.MatchingFields{ModelNameIndexKey: modelName}, client.InNamespace(ds.pool.Namespace)); err != nil {
 		return false, fmt.Errorf("listing models that match the modelName %s: %w", modelName, err)
 	}
 	if len(models.Items) == 0 {
@@ -288,10 +288,10 @@ func (ds *datastore) PodDelete(namespacedName types.NamespacedName) {
 	}
 }
 
-func (ds *datastore) podResyncAll(ctx context.Context, ctrlClient client.Client) error {
+func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) error {
 	logger := log.FromContext(ctx)
 	podList := &corev1.PodList{}
-	if err := ctrlClient.List(ctx, podList, &client.ListOptions{
+	if err := reader.List(ctx, podList, &client.ListOptions{
 		LabelSelector: selectorFromInferencePoolSelector(ds.pool.Spec.Selector),
 		Namespace:     ds.pool.Namespace,
 	}); err != nil {
diff --git a/pkg/epp/handlers/response.go b/pkg/epp/handlers/response.go
index 7284628..a776bd1 100644
--- a/pkg/epp/handlers/response.go
+++ b/pkg/epp/handlers/response.go
@@ -34,11 +34,7 @@ const (
 )
 
 // HandleResponseBody always returns the requestContext even in the error case, as the request context is used in error handling.
-func (s *StreamingServer) HandleResponseBody(
-	ctx context.Context,
-	reqCtx *RequestContext,
-	response map[string]interface{},
-) (*RequestContext, error) {
+func (s *StreamingServer) HandleResponseBody(ctx context.Context, reqCtx *RequestContext, response map[string]any) (*RequestContext, error) {
 	logger := log.FromContext(ctx)
 	responseBytes, err := json.Marshal(response)
 	if err != nil {
@@ -46,7 +42,7 @@ func (s *StreamingServer) HandleResponseBody(
 		return reqCtx, err
 	}
 	if response["usage"] != nil {
-		usg := response["usage"].(map[string]interface{})
+		usg := response["usage"].(map[string]any)
 		usage := Usage{
 			PromptTokens:     int(usg["prompt_tokens"].(float64)),
 			CompletionTokens: int(usg["completion_tokens"].(float64)),
@@ -68,11 +64,7 @@ func (s *StreamingServer) HandleResponseBody(
 }
 
 // The function is to handle streaming response if the modelServer is streaming.
-func (s *StreamingServer) HandleResponseBodyModelStreaming(
-	ctx context.Context,
-	reqCtx *RequestContext,
-	responseText string,
-) {
+func (s *StreamingServer) HandleResponseBodyModelStreaming(ctx context.Context, reqCtx *RequestContext, responseText string) {
 	if strings.Contains(responseText, streamingEndMsg) {
 		resp := parseRespForUsage(ctx, responseText)
 		reqCtx.Usage = resp.Usage
@@ -160,10 +152,7 @@ func (s *StreamingServer) generateResponseHeaders(reqCtx *RequestContext) []*con
 //
 // If include_usage is not included in the request, `data: [DONE]` is returned separately, which
 // indicates end of streaming.
-func parseRespForUsage(
-	ctx context.Context,
-	responseText string,
-) ResponseBody {
+func parseRespForUsage(ctx context.Context, responseText string) ResponseBody {
 	response := ResponseBody{}
 	logger := log.FromContext(ctx)
 
diff --git a/pkg/epp/handlers/server.go b/pkg/epp/handlers/server.go
index e31b53a..3ac13c8 100644
--- a/pkg/epp/handlers/server.go
+++ b/pkg/epp/handlers/server.go
@@ -112,7 +112,7 @@ type RequestContext struct {
 
 type Request struct {
 	Headers  map[string]string
-	Body     map[string]interface{}
+	Body     map[string]any
 	Metadata map[string]any
 }
 type Response struct {
@@ -143,7 +143,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 		RequestState: RequestReceived,
 		Request: &Request{
 			Headers:  make(map[string]string),
-			Body:     make(map[string]interface{}),
+			Body:     make(map[string]any),
 			Metadata: make(map[string]any),
 		},
 		Response: &Response{
@@ -152,7 +152,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 	}
 
 	var body []byte
-	var responseBody map[string]interface{}
+	var responseBody map[string]any
 
 	// Create error handling var as each request should only report once for
 	// error metrics. This doesn't cover the error "Cannot receive stream request" because
@@ -308,7 +308,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 		// Handle the err and fire an immediate response.
 		if err != nil {
 			logger.V(logutil.DEFAULT).Error(err, "Failed to process request", "request", req)
-			resp, err := BuildErrResponse(err)
+			resp, err := buildErrResponse(err)
 			if err != nil {
 				return err
 			}
@@ -389,7 +389,7 @@ func (r *RequestContext) updateStateAndSendIfNeeded(srv extProcPb.ExternalProces
 	return nil
 }
 
-func BuildErrResponse(err error) (*extProcPb.ProcessingResponse, error) {
+func buildErrResponse(err error) (*extProcPb.ProcessingResponse, error) {
 	var resp *extProcPb.ProcessingResponse
 
 	switch errutil.CanonicalCode(err) {
@@ -416,6 +416,17 @@ func BuildErrResponse(err error) (*extProcPb.ProcessingResponse, error) {
 				},
 			},
 		}
+	// This code can be returned by the director when there are no candidate pods for the request scheduling.
+	case errutil.ServiceUnavailable:
+		resp = &extProcPb.ProcessingResponse{
+			Response: &extProcPb.ProcessingResponse_ImmediateResponse{
+				ImmediateResponse: &extProcPb.ImmediateResponse{
+					Status: &envoyTypePb.HttpStatus{
+						Code: envoyTypePb.StatusCode_ServiceUnavailable,
+					},
+				},
+			},
+		}
 	// This code can be returned when users provide invalid json request.
 	case errutil.BadRequest:
 		resp = &extProcPb.ProcessingResponse{
diff --git a/pkg/epp/metrics/metrics.go b/pkg/epp/metrics/metrics.go
index 50f6374..7295b15 100644
--- a/pkg/epp/metrics/metrics.go
+++ b/pkg/epp/metrics/metrics.go
@@ -400,7 +400,7 @@ func RecordInferencePoolAvgQueueSize(name string, queueSize float64) {
 	inferencePoolAvgQueueSize.WithLabelValues(name).Set(queueSize)
 }
 
-func RecordinferencePoolReadyPods(name string, runningPods float64) {
+func RecordInferencePoolReadyPods(name string, runningPods float64) {
 	inferencePoolReadyPods.WithLabelValues(name).Set(runningPods)
 }
 
diff --git a/pkg/epp/plugins/handle.go b/pkg/epp/plugins/handle.go
new file mode 100644
index 0000000..f00aa50
--- /dev/null
+++ b/pkg/epp/plugins/handle.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plugins
+
+import (
+	"context"
+	"fmt"
+)
+
+// Handle provides plugins a set of standard data and tools to work with
+type Handle interface {
+	// Context returns a context the plugins can use, if they need one
+	Context() context.Context
+
+	HandlePlugins
+}
+
+// HandlePlugins defines a set of APIs to work with instantiated plugins
+type HandlePlugins interface {
+	// Plugin returns the named plugin instance
+	Plugin(name string) Plugin
+
+	// AddPlugin adds a plugin to the set of known plugin instances
+	AddPlugin(name string, plugin Plugin)
+
+	// GetAllPlugins returns all of the known plugins
+	GetAllPlugins() []Plugin
+
+	// GetAllPluginsWithNames returns all of the known plugins with their names
+	GetAllPluginsWithNames() map[string]Plugin
+}
+
+// PluginByType retrieves the specified plugin by name and verifies its type
+func PluginByType[P Plugin](handlePlugins HandlePlugins, name string) (P, error) {
+	var zero P
+
+	rawPlugin := handlePlugins.Plugin(name)
+	if rawPlugin == nil {
+		return zero, fmt.Errorf("there is no plugin with the name '%s' defined", name)
+	}
+	plugin, ok := rawPlugin.(P)
+	if !ok {
+		return zero, fmt.Errorf("the plugin with the name '%s' is not an instance of %T", name, zero)
+	}
+	return plugin, nil
+}
diff --git a/pkg/epp/plugins/plugins.go b/pkg/epp/plugins/plugins.go
index ac6bca9..93ed23d 100644
--- a/pkg/epp/plugins/plugins.go
+++ b/pkg/epp/plugins/plugins.go
@@ -16,37 +16,9 @@ limitations under the License.
 
 package plugins
 
-import "context"
-
 // Plugin defines the interface for a plugin.
 // This interface should be embedded in all plugins across the code.
 type Plugin interface {
-	// Type returns the type of the plugin.
-	Type() string
-	// Name returns the name of this plugin instance.
-	Name() string
-}
-
-// Handle provides plugins a set of standard data and tools to work with
-type Handle interface {
-	// Context returns a context the plugins can use, if they need one
-	Context() context.Context
-
-	// Plugins returns the sub-handle for working with instantiated plugins
-	Plugins() HandlePlugins
-}
-
-// HandlePlugins defines a set of APIs to work with instantiated plugins
-type HandlePlugins interface {
-	// Plugin returns the named plugin instance
-	Plugin(name string) Plugin
-
-	// AddPlugin adds a plugin to the set of known plugin instances
-	AddPlugin(name string, plugin Plugin)
-
-	// GetAllPlugins returns all of the known plugins
-	GetAllPlugins() []Plugin
-
-	// GetAllPluginsWithNames returns all of the known plugins with their names
-	GetAllPluginsWithNames() map[string]Plugin
+	// TypedName returns the type and name tuple of this plugin instance.
+	TypedName() TypedName
 }
diff --git a/pkg/epp/plugins/typedname.go b/pkg/epp/plugins/typedname.go
new file mode 100644
index 0000000..174d97e
--- /dev/null
+++ b/pkg/epp/plugins/typedname.go
@@ -0,0 +1,34 @@
+/*
+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
+
+const (
+	separator = "/"
+)
+
+// TypedName is a utility struct providing a type and a name to plugins.
+type TypedName struct {
+	// Type returns the type of a plugin.
+	Type string
+	// Name returns the name of a plugin instance.
+	Name string
+}
+
+// String returns the type and name rendered as "<name>/<type>".
+func (tn *TypedName) String() string {
+	return tn.Name + separator + tn.Type
+}
diff --git a/pkg/epp/requestcontrol/director.go b/pkg/epp/requestcontrol/director.go
index 59cc442..670d922 100644
--- a/pkg/epp/requestcontrol/director.go
+++ b/pkg/epp/requestcontrol/director.go
@@ -24,12 +24,14 @@ import (
 	"math/rand"
 	"net"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/go-logr/logr"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
@@ -39,6 +41,11 @@ import (
 	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
 )
 
+const (
+	subsetHintNamespace = "envoy.lb.subset_hint"
+	subsetHintKey       = "x-gateway-destination-endpoint-subset"
+)
+
 // Scheduler defines the interface required by the Director for scheduling.
 type Scheduler interface {
 	Schedule(ctx context.Context, request *schedulingtypes.LLMRequest, candidatePods []schedulingtypes.Pod) (result *schedulingtypes.SchedulingResult, err error)
@@ -73,7 +80,7 @@ type Director struct {
 //  1. Parses request details.
 //  2. Calls admitRequest for admission control.
 //  3. Calls Scheduler.Schedule if request is approved.
-//  4. Calls prepareRequest to populate RequestContext with results and call PreRequest plugins.
+//  4. Calls prepareRequest to populate RequestContext with result and call PreRequest plugins.
 //
 // It always returns the requestContext even in the error case, as the request context is used in error handling.
 func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestContext) (*handlers.RequestContext, error) {
@@ -118,12 +125,12 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	}
 
 	// Prepare LLMRequest (needed for both saturation detection and Scheduler)
-	reqCtx.SchedulingRequest = schedulingtypes.NewLLMRequest(
-		reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
-		reqCtx.ResolvedTargetModel,
-		prompt,
-		reqCtx.Request.Headers,
-		reqCtx.Request.Metadata)
+	reqCtx.SchedulingRequest = &schedulingtypes.LLMRequest{
+		RequestId:   reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
+		TargetModel: reqCtx.ResolvedTargetModel,
+		Prompt:      prompt,
+		Headers:     reqCtx.Request.Headers,
+	}
 
 	logger = logger.WithValues("model", reqCtx.Model, "resolvedTargetModel", reqCtx.ResolvedTargetModel, "criticality", requestCriticality)
 
@@ -135,12 +142,12 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 		return reqCtx, err
 	}
 
-	// --- 3. Call Scheduler ---
-	// Snapshot pod metrics from the datastore to:
-	// 1. Reduce concurrent access to the datastore.
-	// 2. Ensure consistent data during the scheduling operation of a request between all scheduling cycles.
-	candidatePods := schedulingtypes.ToSchedulerPodMetrics(d.datastore.PodGetAll())
-	results, err := d.scheduler.Schedule(ctx, reqCtx.SchedulingRequest, candidatePods)
+	// --- 3. Call Scheduler (with the relevant candidate pods) ---
+	candidatePods := d.getCandidatePodsForScheduling(ctx, reqCtx.Request.Metadata)
+	if len(candidatePods) == 0 {
+		return reqCtx, errutil.Error{Code: errutil.ServiceUnavailable, Msg: "failed to find candidate pods for serving the request"}
+	}
+	result, err := d.scheduler.Schedule(ctx, reqCtx.SchedulingRequest, candidatePods)
 	if err != nil {
 		return reqCtx, errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: fmt.Errorf("failed to find target pod: %w", err).Error()}
 	}
@@ -148,7 +155,7 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	// --- 4. Prepare Request (Populates RequestContext and call PreRequest plugins) ---
 	// Insert target endpoint to instruct Envoy to route requests to the specified target pod and attach the port number.
 	// Invoke PreRequest registered plugins.
-	reqCtx, err = d.prepareRequest(ctx, reqCtx, results)
+	reqCtx, err = d.prepareRequest(ctx, reqCtx, result)
 	if err != nil {
 		return reqCtx, err
 	}
@@ -177,15 +184,62 @@ func (d *Director) admitRequest(ctx context.Context, requestCriticality v1alpha2
 	return nil
 }
 
+// getCandidatePodsForScheduling gets the list of relevant endpoints for the scheduling cycle from the datastore.
+// according to EPP protocol, if "x-gateway-destination-endpoint-subset" is set on the request metadata and specifies
+// a subset of endpoints, only these endpoints will be considered as candidates for the scheduler.
+// Snapshot pod metrics from the datastore to:
+// 1. Reduce concurrent access to the datastore.
+// 2. Ensure consistent data during the scheduling operation of a request between all scheduling cycles.
+func (d *Director) getCandidatePodsForScheduling(ctx context.Context, requestMetadata map[string]any) []schedulingtypes.Pod {
+	loggerTrace := log.FromContext(ctx).V(logutil.TRACE)
+
+	subsetMap, found := requestMetadata[subsetHintNamespace].(map[string]any)
+	if !found {
+		return d.toSchedulerPodMetrics(d.datastore.PodGetAll())
+	}
+
+	// Check if endpoint key is present in the subset map and ensure there is at least one value
+	endpointSubsetList, found := subsetMap[subsetHintKey].([]any)
+	if !found {
+		return d.toSchedulerPodMetrics(d.datastore.PodGetAll())
+	} else if len(endpointSubsetList) == 0 {
+		loggerTrace.Info("found empty subset filter in request metadata, filtering all pods")
+		return []schedulingtypes.Pod{}
+	}
+
+	// Create a map of endpoint addresses for easy lookup
+	endpoints := make(map[string]bool)
+	for _, endpoint := range endpointSubsetList {
+		// Extract address from endpoint
+		// The endpoint is formatted as "<address>:<port>" (ex. "10.0.1.0:8080")
+		epStr := strings.Split(endpoint.(string), ":")[0]
+		endpoints[epStr] = true
+	}
+
+	podTotalCount := 0
+	podFitleredList := d.datastore.PodList(func(pm backendmetrics.PodMetrics) bool {
+		podTotalCount++
+		if _, found := endpoints[pm.GetPod().Address]; found {
+			return true
+		}
+		return false
+	})
+
+	loggerTrace.Info("filtered candidate pods by subset filtering", "podTotalCount", podTotalCount, "filteredCount", len(podFitleredList))
+
+	return d.toSchedulerPodMetrics(podFitleredList)
+}
+
 // prepareRequest populates the RequestContext and calls the registered PreRequest plugins
-// for allowing plugging customized logic based on the scheduling results.
+// for allowing plugging customized logic based on the scheduling result.
 func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestContext, result *schedulingtypes.SchedulingResult) (*handlers.RequestContext, error) {
 	logger := log.FromContext(ctx)
 	if result == nil || len(result.ProfileResults) == 0 {
 		return reqCtx, errutil.Error{Code: errutil.Internal, Msg: "results must be greater than zero"}
 	}
 	// primary profile is used to set destination
-	targetPod := result.ProfileResults[result.PrimaryProfileName].TargetPod.GetPod()
+	// TODO should use multiple destinations according to epp protocol. current code assumes a single target
+	targetPod := result.ProfileResults[result.PrimaryProfileName].TargetPods[0].GetPod()
 
 	pool, err := d.datastore.PoolGet()
 	if err != nil {
@@ -204,6 +258,15 @@ func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestC
 	return reqCtx, nil
 }
 
+func (d *Director) toSchedulerPodMetrics(pods []backendmetrics.PodMetrics) []schedulingtypes.Pod {
+	pm := make([]schedulingtypes.Pod, len(pods))
+	for i, pod := range pods {
+		pm[i] = &schedulingtypes.PodMetrics{Pod: pod.GetPod().Clone(), MetricsState: pod.GetMetrics().Clone()}
+	}
+
+	return pm
+}
+
 func (d *Director) HandleResponse(ctx context.Context, reqCtx *handlers.RequestContext) (*handlers.RequestContext, error) {
 	response := &Response{
 		RequestId: reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
@@ -257,20 +320,21 @@ func RandomWeightedDraw(logger logr.Logger, model *v1alpha2.InferenceModel, seed
 }
 
 func (d *Director) runPreRequestPlugins(ctx context.Context, request *schedulingtypes.LLMRequest, schedulingResult *schedulingtypes.SchedulingResult,
-	targetPort int) {
+	targetPort int,
+) {
 	for _, plugin := range d.preRequestPlugins {
-		log.FromContext(ctx).V(logutil.DEBUG).Info("Running pre-request plugin", "plugin", plugin.Type())
+		log.FromContext(ctx).V(logutil.DEBUG).Info("Running pre-request plugin", "plugin", plugin.TypedName().Type)
 		before := time.Now()
 		plugin.PreRequest(ctx, request, schedulingResult, targetPort)
-		metrics.RecordRequestControlPluginProcessingLatency(PreRequestPluginType, plugin.Type(), time.Since(before))
+		metrics.RecordRequestControlPluginProcessingLatency(PreRequestPluginType, plugin.TypedName().Type, 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.Type())
+		log.FromContext(ctx).V(logutil.DEBUG).Info("Running post-response plugin", "plugin", plugin.TypedName().Type)
 		before := time.Now()
 		plugin.PostResponse(ctx, request, response, targetPod)
-		metrics.RecordRequestControlPluginProcessingLatency(PostResponsePluginType, plugin.Type(), time.Since(before))
+		metrics.RecordRequestControlPluginProcessingLatency(PostResponsePluginType, plugin.TypedName().Type, time.Since(before))
 	}
 }
diff --git a/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go b/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go
index e73d5f9..9c29995 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/decision_tree_filter.go
@@ -36,9 +36,11 @@ const (
 // compile-time type assertion
 var _ framework.Filter = &DecisionTreeFilter{}
 
-// DecisionTreeFilter applies current fitler, and then recursively applies next filters
+// DecisionTreeFilter applies current filter, and then recursively applies next filters
 // depending success or failure of the current filter.
 // It can be used to construct a flow chart algorithm.
+// Since a DecisionTreeFilter takes on the type and name of the current filter,
+// it is not embedding a fixed plugins.TypeName.
 type DecisionTreeFilter struct {
 	Current framework.Filter
 	// NextOnSuccess filter will be applied after successfully applying the current filter.
@@ -117,7 +119,7 @@ func loadDecisionTreeEntry(entry *decisionTreeFilterEntry, handle plugins.Handle
 	}
 
 	if entry.PluginRef != nil {
-		instance := handle.Plugins().Plugin(*entry.PluginRef)
+		instance := handle.Plugin(*entry.PluginRef)
 		if instance == nil {
 			return nil, errors.New(*entry.PluginRef + " is a reference to an undefined Plugin")
 		}
@@ -131,20 +133,14 @@ func loadDecisionTreeEntry(entry *decisionTreeFilterEntry, handle plugins.Handle
 	return nil, errors.New("either pluginRef or decisionTree must be specified")
 }
 
-// Type returns the type of the filter.
-func (f *DecisionTreeFilter) Type() string {
+func (f *DecisionTreeFilter) TypedName() plugins.TypedName {
 	if f == nil {
-		return "nil"
+		// TODO: this keeps the previous behavior ("nil"/"") - not sure
+		// why done this way.
+		// Change to empty TypedName or some more meaningful values?
+		return plugins.TypedName{Type: "nil", Name: ""}
 	}
-	return f.Current.Type()
-}
-
-// Name returns the name of the filter.
-func (f *DecisionTreeFilter) Name() string {
-	if f == nil {
-		return ""
-	}
-	return f.Current.Name()
+	return f.Current.TypedName()
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
@@ -161,7 +157,7 @@ func (f *DecisionTreeFilter) Filter(ctx context.Context, cycleState *types.Cycle
 		if f.NextOnSuccess != nil {
 			next = f.NextOnSuccess
 		}
-		loggerTrace.Info("Filter succeeded", "filter", f.Type(), "next", next.Type(), "filteredPodCount", len(filteredPod))
+		loggerTrace.Info("Filter succeeded", "filter", f.TypedName(), "next", next.TypedName(), "filteredPodCount", len(filteredPod))
 		// On success, pass the filtered result to the next filter.
 		return next.Filter(ctx, cycleState, request, filteredPod)
 	} else {
@@ -172,7 +168,7 @@ func (f *DecisionTreeFilter) Filter(ctx context.Context, cycleState *types.Cycle
 		if f.NextOnFailure != nil {
 			next = f.NextOnFailure
 		}
-		loggerTrace.Info("Filter failed", "filter", f.Type(), "next", next.Type())
+		loggerTrace.Info("Filter failed", "filter", f.TypedName(), "next", next.TypedName())
 		// On failure, pass the initial set of pods to the next filter.
 		return next.Filter(ctx, cycleState, request, pods)
 	}
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 ffb190c..3cf9bb6 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/least_kvcache_filter.go
@@ -41,7 +41,7 @@ func LeastKVCacheFilterFactory(name string, _ json.RawMessage, _ plugins.Handle)
 // NewLeastKVCacheFilter initializes a new LeastKVCacheFilter and returns its pointer.
 func NewLeastKVCacheFilter() *LeastKVCacheFilter {
 	return &LeastKVCacheFilter{
-		name: LeastKVCacheFilterType,
+		tn: plugins.TypedName{Type: LeastKVCacheFilterType, Name: LeastKVCacheFilterType},
 	}
 }
 
@@ -51,22 +51,17 @@ func NewLeastKVCacheFilter() *LeastKVCacheFilter {
 // should consider them all instead of the absolute minimum one. This worked better than picking the
 // least one as it gives more choices for the next filter, which on aggregate gave better results.
 type LeastKVCacheFilter struct {
-	name string
+	tn plugins.TypedName
 }
 
-// Type returns the type of the filter.
-func (f *LeastKVCacheFilter) Type() string {
-	return LeastKVCacheFilterType
-}
-
-// Name returns the name of the filter.
-func (f *LeastKVCacheFilter) Name() string {
-	return f.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (f *LeastKVCacheFilter) TypedName() plugins.TypedName {
+	return f.tn
 }
 
 // WithName sets the name of the filter.
 func (f *LeastKVCacheFilter) WithName(name string) *LeastKVCacheFilter {
-	f.name = name
+	f.tn.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go b/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
index 54b619c..387cc59 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/least_queue_filter.go
@@ -41,7 +41,7 @@ func LeastQueueFilterFactory(name string, _ json.RawMessage, _ plugins.Handle) (
 // NewLeastQueueFilter initializes a new LeastQueueFilter and returns its pointer.
 func NewLeastQueueFilter() *LeastQueueFilter {
 	return &LeastQueueFilter{
-		name: LeastQueueFilterType,
+		tn: plugins.TypedName{Type: LeastQueueFilterType, Name: LeastQueueFilterType},
 	}
 }
 
@@ -51,22 +51,17 @@ func NewLeastQueueFilter() *LeastQueueFilter {
 // we should consider them all instead of the absolute minimum one. This worked better than picking
 // the least one as it gives more choices for the next filter, which on aggregate gave better results.
 type LeastQueueFilter struct {
-	name string
+	tn plugins.TypedName
 }
 
-// Type returns the type of the filter.
-func (f *LeastQueueFilter) Type() string {
-	return LeastQueueFilterType
-}
-
-// Name returns the name of the filter.
-func (f *LeastQueueFilter) Name() string {
-	return f.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (f *LeastQueueFilter) TypedName() plugins.TypedName {
+	return f.tn
 }
 
 // WithName sets the name of the filter.
 func (f *LeastQueueFilter) WithName(name string) *LeastQueueFilter {
-	f.name = name
+	f.tn.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go b/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
index edb18a9..037cce2 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/lora_affinity_filter.go
@@ -43,8 +43,10 @@ var _ framework.Filter = &LoraAffinityFilter{}
 // LoraAffinityFilterFactory defines the factory function for LoraAffinityFilter.
 func LoraAffinityFilterFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
 	parameters := loraAffinityFilterParameters{Threshold: config.DefaultLoraAffinityThreshold}
-	if err := json.Unmarshal(rawParameters, &parameters); err != nil {
-		return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LoraAffinityFilterType, err)
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LoraAffinityFilterType, err)
+		}
 	}
 	return NewLoraAffinityFilter(parameters.Threshold).WithName(name), nil
 }
@@ -52,7 +54,7 @@ func LoraAffinityFilterFactory(name string, rawParameters json.RawMessage, _ plu
 // NewLoraAffinityFilter initializes a new LoraAffinityFilter and returns its pointer.
 func NewLoraAffinityFilter(threshold float64) *LoraAffinityFilter {
 	return &LoraAffinityFilter{
-		name:                  LoraAffinityFilterType,
+		tn:                    plugins.TypedName{Type: LoraAffinityFilterType, Name: LoraAffinityFilterType},
 		loraAffinityThreshold: threshold,
 	}
 }
@@ -65,23 +67,18 @@ func NewLoraAffinityFilter(threshold float64) *LoraAffinityFilter {
 // 2. Using a probability threshold to sometimes select from non-affinity pods to enable load balancing
 // 3. Falling back to whatever group has pods if one group is empty
 type LoraAffinityFilter struct {
-	name                  string
+	tn                    plugins.TypedName
 	loraAffinityThreshold float64
 }
 
-// Type returns the type of the filter.
-func (f *LoraAffinityFilter) Type() string {
-	return LoraAffinityFilterType
-}
-
-// Name returns the type of the filter.
-func (f *LoraAffinityFilter) Name() string {
-	return f.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (f *LoraAffinityFilter) TypedName() plugins.TypedName {
+	return f.tn
 }
 
 // WithName sets the type of the filter.
 func (f *LoraAffinityFilter) WithName(name string) *LoraAffinityFilter {
-	f.name = name
+	f.tn.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go b/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
index b72ccf8..e3f509c 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/low_queue_filter.go
@@ -42,8 +42,10 @@ var _ framework.Filter = &LowQueueFilter{}
 // LowQueueFilterFactory defines the factory function for LowQueueFilter.
 func LowQueueFilterFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
 	parameters := lowQueueFilterParameters{Threshold: config.DefaultQueueingThresholdLoRA}
-	if err := json.Unmarshal(rawParameters, &parameters); err != nil {
-		return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LowQueueFilterType, err)
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' filter - %w", LowQueueFilterType, err)
+		}
 	}
 
 	return NewLowQueueFilter(parameters.Threshold).WithName(name), nil
@@ -52,30 +54,25 @@ func LowQueueFilterFactory(name string, rawParameters json.RawMessage, _ plugins
 // NewLowQueueFilter initializes a new LowQueueFilter and returns its pointer.
 func NewLowQueueFilter(threshold int) *LowQueueFilter {
 	return &LowQueueFilter{
-		name:                  LowQueueFilterType,
+		tn:                    plugins.TypedName{Type: LowQueueFilterType, Name: LowQueueFilterType},
 		queueingThresholdLoRA: threshold,
 	}
 }
 
 // LowQueueFilter returns pods that their waiting queue size is less than a configured threshold
 type LowQueueFilter struct {
-	name                  string
+	tn                    plugins.TypedName
 	queueingThresholdLoRA int
 }
 
-// Type returns the type of the filter.
-func (f *LowQueueFilter) Type() string {
-	return LowQueueFilterType
-}
-
-// Name returns the name of the filter.
-func (f *LowQueueFilter) Name() string {
-	return f.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (f *LowQueueFilter) TypedName() plugins.TypedName {
+	return f.tn
 }
 
 // WithName sets the name of the filter.
 func (f *LowQueueFilter) WithName(name string) *LowQueueFilter {
-	f.name = name
+	f.tn.Name = name
 	return f
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/filter/subsetting_filter.go b/pkg/epp/scheduling/framework/plugins/filter/subsetting_filter.go
deleted file mode 100644
index 2962b95..0000000
--- a/pkg/epp/scheduling/framework/plugins/filter/subsetting_filter.go
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
-Copyright 2025 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package filter
-
-import (
-	"context"
-	"strings"
-
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
-)
-
-const (
-	SubsetFilterType = "subset"
-
-	subsetHintKey       = "x-gateway-destination-endpoint-subset"
-	subsetHintNamespace = "envoy.lb.subset_hint"
-)
-
-// compile-time type assertion
-var _ framework.Filter = &SubsetFilter{}
-
-// NewSubsetFilter initializes a new SubsetFilter.
-func NewSubsetFilter() *SubsetFilter {
-	return &SubsetFilter{}
-}
-
-// SubsetFilter filters Pods based on the subset hint provided by the proxy via filterMetadata.
-type SubsetFilter struct{}
-
-// Name returns the name of the filter.
-func (f *SubsetFilter) Name() string {
-	return "subset-hint"
-}
-
-// Type returns the type of the filter.
-func (f *SubsetFilter) Type() string {
-	return SubsetFilterType
-}
-
-// Filter filters out pods that are not in the subset provided in filterMetadata.
-func (f *SubsetFilter) Filter(_ context.Context, _ *types.CycleState, request *types.LLMRequest, pods []types.Pod) []types.Pod {
-	// Check if subset namespace key is present in the metadata map
-	subsetMap, found := request.GetMetadata()[subsetHintNamespace].(map[string]any)
-	if !found {
-		return pods
-	}
-
-	// Check if endpoint key is present in the subset map and ensure there is at least one value
-	endpointSubsetList, found := subsetMap[subsetHintKey].([]interface{})
-	if !found {
-		return pods
-	} else if len(endpointSubsetList) == 0 {
-		return []types.Pod{}
-	}
-
-	// Create a map of endpoint addrs for easy lookup
-	endpoints := make(map[string]bool)
-	for _, endpoint := range endpointSubsetList {
-		// Extract address from endpoint
-		// The endpoint is formatted as "<address>:<port>" (ex. "10.0.1.0:8080")
-		epStr := strings.Split(endpoint.(string), ":")[0]
-		endpoints[epStr] = true
-	}
-
-	// Filter based on address
-	filteredPods := []types.Pod{}
-	for _, pod := range pods {
-		if _, found := endpoints[pod.GetPod().Address]; found {
-			filteredPods = append(filteredPods, pod)
-		}
-	}
-
-	return filteredPods
-}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
index d7fe5d1..5fd77d2 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
@@ -68,7 +68,7 @@ type Config struct {
 
 type Plugin struct {
 	Config
-	name    string
+	tn      plugins.TypedName
 	indexer Indexer
 }
 
@@ -125,8 +125,11 @@ func PrefixCachePluginFactory(name string, rawParameters json.RawMessage, _ plug
 		MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
-	if err := json.Unmarshal(rawParameters, &parameters); err != nil {
-		return nil, fmt.Errorf("failed to parse the parameters of the %s plugin. Error: %s", PrefixCachePluginType, err)
+
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the %s plugin. Error: %s", PrefixCachePluginType, err)
+		}
 	}
 
 	return New(parameters).WithName(name), nil
@@ -144,25 +147,20 @@ func New(config Config) *Plugin {
 	}
 
 	return &Plugin{
-		name:    PrefixCachePluginType,
+		tn:      plugins.TypedName{Type: PrefixCachePluginType, Name: PrefixCachePluginType},
 		Config:  config,
 		indexer: newIndexer(capacity),
 	}
 }
 
-// Type returns the type of the plugin.
-func (m *Plugin) Type() string {
-	return PrefixCachePluginType
-}
-
-// Name returns the name of the plugin.
-func (m *Plugin) Name() string {
-	return m.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (m *Plugin) TypedName() plugins.TypedName {
+	return m.tn
 }
 
 // WithName sets the name of the plugin.
 func (m *Plugin) WithName(name string) *Plugin {
-	m.name = name
+	m.tn.Name = name
 	return m
 }
 
@@ -176,7 +174,7 @@ func (m *Plugin) Score(ctx context.Context, cycleState *types.CycleState, reques
 		PrefixCacheServers: m.matchLongestPrefix(ctx, hashes),
 	}
 
-	cycleState.Write(types.StateKey(m.Type()), state)
+	cycleState.Write(types.StateKey(m.TypedName().Type), state)
 	loggerTrace.Info(fmt.Sprintf("cached servers: %+v", state.PrefixCacheServers), "hashes", state.PrefixHashes)
 	// calculate the scores of pods
 	scores := make(map[types.Pod]float64, len(pods))
@@ -198,7 +196,7 @@ func (m *Plugin) Score(ctx context.Context, cycleState *types.CycleState, reques
 
 // PostCycle records in the plugin cache the result of the scheduling selection.
 func (m *Plugin) PostCycle(ctx context.Context, cycleState *types.CycleState, res *types.ProfileRunResult) {
-	targetPod := res.TargetPod.GetPod()
+	targetPod := res.TargetPods[0].GetPod()
 	state, err := types.ReadCycleStateKey[*SchedulingContextState](cycleState, PrefixCachePluginType)
 	if err != nil {
 		log.FromContext(ctx).Error(err, "failed to read prefix plugin cycle state")
diff --git a/pkg/epp/scheduling/framework/plugins/picker/common.go b/pkg/epp/scheduling/framework/plugins/picker/common.go
new file mode 100644
index 0000000..4bbc300
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/picker/common.go
@@ -0,0 +1,26 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package picker
+
+const (
+	DefaultMaxNumOfEndpoints = 1 // common default to all pickers
+)
+
+// pickerParameters defines the common parameters for all pickers
+type pickerParameters struct {
+	MaxNumOfEndpoints int `json:"maxNumOfEndpoints"`
+}
diff --git a/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
index db4e848..4faac2b 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
@@ -20,8 +20,10 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"slices"
 
 	"sigs.k8s.io/controller-runtime/pkg/log"
+
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
@@ -36,58 +38,71 @@ const (
 var _ framework.Picker = &MaxScorePicker{}
 
 // MaxScorePickerFactory defines the factory function for MaxScorePicker.
-func MaxScorePickerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
-	return NewMaxScorePicker().WithName(name), nil
+func MaxScorePickerFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	parameters := pickerParameters{MaxNumOfEndpoints: DefaultMaxNumOfEndpoints}
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' picker - %w", MaxScorePickerType, err)
+		}
+	}
+
+	return NewMaxScorePicker(parameters.MaxNumOfEndpoints).WithName(name), nil
 }
 
 // NewMaxScorePicker initializes a new MaxScorePicker and returns its pointer.
-func NewMaxScorePicker() *MaxScorePicker {
+func NewMaxScorePicker(maxNumOfEndpoints int) *MaxScorePicker {
+	if maxNumOfEndpoints <= 0 {
+		maxNumOfEndpoints = DefaultMaxNumOfEndpoints // on invalid configuration value, fallback to default value
+	}
+
 	return &MaxScorePicker{
-		name:   MaxScorePickerType,
-		random: NewRandomPicker(),
+		typedName:         plugins.TypedName{Type: MaxScorePickerType, Name: MaxScorePickerType},
+		maxNumOfEndpoints: maxNumOfEndpoints,
 	}
 }
 
-// MaxScorePicker picks the pod with the maximum score from the list of candidates.
+// MaxScorePicker picks pod(s) with the maximum score from the list of candidates.
 type MaxScorePicker struct {
-	name   string
-	random *RandomPicker
-}
-
-// Type returns the type of the picker.
-func (p *MaxScorePicker) Type() string {
-	return MaxScorePickerType
-}
-
-// Name returns the name of the picker.
-func (p *MaxScorePicker) Name() string {
-	return p.name
+	typedName         plugins.TypedName
+	maxNumOfEndpoints int // maximum number of endpoints to pick
 }
 
 // WithName sets the picker's name
 func (p *MaxScorePicker) WithName(name string) *MaxScorePicker {
-	p.name = name
+	p.typedName.Name = name
 	return p
 }
 
+// TypedName returns the type and name tuple of this plugin instance.
+func (p *MaxScorePicker) TypedName() plugins.TypedName {
+	return p.typedName
+}
+
 // Pick selects the pod with the maximum score from the list of candidates.
 func (p *MaxScorePicker) Pick(ctx context.Context, cycleState *types.CycleState, scoredPods []*types.ScoredPod) *types.ProfileRunResult {
-	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a pod with the max score from %d candidates: %+v", len(scoredPods), scoredPods))
-
-	highestScorePods := []*types.ScoredPod{}
-	maxScore := -1.0 // pods min score is 0, putting value lower than 0 in order to find at least one pod as highest
-	for _, pod := range scoredPods {
-		if pod.Score > maxScore {
-			maxScore = pod.Score
-			highestScorePods = []*types.ScoredPod{pod}
-		} else if pod.Score == maxScore {
-			highestScorePods = append(highestScorePods, pod)
+	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting maximum '%d' pods from %d candidates sorted by max score: %+v", p.maxNumOfEndpoints,
+		len(scoredPods), scoredPods))
+
+	slices.SortStableFunc(scoredPods, func(i, j *types.ScoredPod) int { // highest score first
+		if i.Score > j.Score {
+			return -1
+		}
+		if i.Score < j.Score {
+			return 1
 		}
+		return 0
+	})
+
+	// if we have enough pods to return keep only the "maxNumOfEndpoints" highest scored pods
+	if p.maxNumOfEndpoints < len(scoredPods) {
+		scoredPods = scoredPods[:p.maxNumOfEndpoints]
 	}
 
-	if len(highestScorePods) > 1 {
-		return p.random.Pick(ctx, cycleState, highestScorePods) // pick randomly from the highest score pods
+	targetPods := make([]types.Pod, len(scoredPods))
+	for i, scoredPod := range scoredPods {
+		targetPods[i] = scoredPod
 	}
 
-	return &types.ProfileRunResult{TargetPod: highestScorePods[0]}
+	return &types.ProfileRunResult{TargetPods: targetPods}
+
 }
diff --git a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
index fb2d44b..56a7f89 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
@@ -23,6 +23,7 @@ import (
 	"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"
@@ -36,42 +37,65 @@ const (
 // compile-time type validation
 var _ framework.Picker = &RandomPicker{}
 
-// RandomPickerFactory defines the factory function for RandomPicker.
-func RandomPickerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
-	return NewRandomPicker().WithName(name), nil
+func RandomPickerFactory(name string, rawParameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	parameters := pickerParameters{MaxNumOfEndpoints: DefaultMaxNumOfEndpoints}
+	if rawParameters != nil {
+		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
+			return nil, fmt.Errorf("failed to parse the parameters of the '%s' picker - %w", RandomPickerType, err)
+		}
+	}
+
+	return NewRandomPicker(parameters.MaxNumOfEndpoints).WithName(name), nil
 }
 
 // NewRandomPicker initializes a new RandomPicker and returns its pointer.
-func NewRandomPicker() *RandomPicker {
+func NewRandomPicker(maxNumOfEndpoints int) *RandomPicker {
+	if maxNumOfEndpoints <= 0 {
+		maxNumOfEndpoints = DefaultMaxNumOfEndpoints // on invalid configuration value, fallback to default value
+	}
+
 	return &RandomPicker{
-		name: RandomPickerType,
+		typedName:         plugins.TypedName{Type: RandomPickerType, Name: RandomPickerType},
+		maxNumOfEndpoints: maxNumOfEndpoints,
 	}
 }
 
-// RandomPicker picks a random pod from the list of candidates.
+// RandomPicker picks random pod(s) from the list of candidates.
 type RandomPicker struct {
-	name string
-}
-
-// Type returns the type of the picker.
-func (p *RandomPicker) Type() string {
-	return RandomPickerType
-}
-
-// Name returns the name of the picker.
-func (p *RandomPicker) Name() string {
-	return p.name
+	typedName         plugins.TypedName
+	maxNumOfEndpoints int
 }
 
-// WithName sets the picker's name
+// WithName sets the name of the picker.
 func (p *RandomPicker) WithName(name string) *RandomPicker {
-	p.name = name
+	p.typedName.Name = name
 	return p
 }
 
-// Pick selects a random pod from the list of candidates.
+// TypedName returns the type and name tuple of this plugin instance.
+func (p *RandomPicker) TypedName() plugins.TypedName {
+	return p.typedName
+}
+
+// Pick selects random pod(s) from the list of candidates.
 func (p *RandomPicker) Pick(ctx context.Context, _ *types.CycleState, scoredPods []*types.ScoredPod) *types.ProfileRunResult {
-	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a random pod from %d candidates: %+v", len(scoredPods), scoredPods))
-	i := rand.Intn(len(scoredPods))
-	return &types.ProfileRunResult{TargetPod: scoredPods[i]}
+	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting maximum '%d' pods from %d candidates randomly: %+v", p.maxNumOfEndpoints,
+		len(scoredPods), scoredPods))
+
+	// Shuffle in-place
+	rand.Shuffle(len(scoredPods), func(i, j int) {
+		scoredPods[i], scoredPods[j] = scoredPods[j], scoredPods[i]
+	})
+
+	// if we have enough pods to return keep only the relevant subset
+	if p.maxNumOfEndpoints < len(scoredPods) {
+		scoredPods = scoredPods[:p.maxNumOfEndpoints]
+	}
+
+	targetPods := make([]types.Pod, len(scoredPods))
+	for i, scoredPod := range scoredPods {
+		targetPods[i] = scoredPod
+	}
+
+	return &types.ProfileRunResult{TargetPods: targetPods}
 }
diff --git a/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go b/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
index 48d75a6..e880948 100644
--- a/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
+++ b/pkg/epp/scheduling/framework/plugins/profile/single_profile_handler.go
@@ -42,28 +42,23 @@ func SingleProfileHandlerFactory(name string, _ json.RawMessage, _ plugins.Handl
 // NewSingleProfileHandler initializes a new SingleProfileHandler and returns its pointer.
 func NewSingleProfileHandler() *SingleProfileHandler {
 	return &SingleProfileHandler{
-		name: SingleProfileHandlerType,
+		tn: plugins.TypedName{Type: SingleProfileHandlerType, Name: SingleProfileHandlerType},
 	}
 }
 
 // SingleProfileHandler handles a single profile which is always the primary profile.
 type SingleProfileHandler struct {
-	name string
+	tn plugins.TypedName
 }
 
-// Type returns the type of the Profile Handler.
-func (h *SingleProfileHandler) Type() string {
-	return SingleProfileHandlerType
-}
-
-// Name returns the name of the profile handler.
-func (h *SingleProfileHandler) Name() string {
-	return h.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (h *SingleProfileHandler) TypedName() plugins.TypedName {
+	return h.tn
 }
 
 // WithName sets the name of the profile handler.
 func (h *SingleProfileHandler) WithName(name string) *SingleProfileHandler {
-	h.name = name
+	h.tn.Name = name
 	return h
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go b/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
index f0d4af9..387ae0b 100644
--- a/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
+++ b/pkg/epp/scheduling/framework/plugins/scorer/kvcache.go
@@ -41,28 +41,23 @@ func KvCacheScorerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plu
 // NewKVCacheScorer initializes a new KVCacheScorer and returns its pointer.
 func NewKVCacheScorer() *KVCacheScorer {
 	return &KVCacheScorer{
-		name: KvCacheScorerType,
+		tn: plugins.TypedName{Type: KvCacheScorerType, Name: KvCacheScorerType},
 	}
 }
 
 // KVCacheScorer scores list of candidate pods based on KV cache utilization.
 type KVCacheScorer struct {
-	name string
+	tn plugins.TypedName
 }
 
-// Type returns the type of the scorer.
-func (s *KVCacheScorer) Type() string {
-	return KvCacheScorerType
-}
-
-// Name returns the name of the scorer.
-func (s *KVCacheScorer) Name() string {
-	return s.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (s *KVCacheScorer) TypedName() plugins.TypedName {
+	return s.tn
 }
 
 // WithName sets the name of the scorer.
 func (s *KVCacheScorer) WithName(name string) *KVCacheScorer {
-	s.name = name
+	s.tn.Name = name
 	return s
 }
 
diff --git a/pkg/epp/scheduling/framework/plugins/scorer/queue.go b/pkg/epp/scheduling/framework/plugins/scorer/queue.go
index dad6d04..c41c1cb 100644
--- a/pkg/epp/scheduling/framework/plugins/scorer/queue.go
+++ b/pkg/epp/scheduling/framework/plugins/scorer/queue.go
@@ -42,29 +42,24 @@ func QueueScorerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugi
 // NewQueueScorer initializes a new QueueScorer and returns its pointer.
 func NewQueueScorer() *QueueScorer {
 	return &QueueScorer{
-		name: QueueScorerType,
+		tn: plugins.TypedName{Type: QueueScorerType, Name: QueueScorerType},
 	}
 }
 
 // QueueScorer scores list of candidate pods based on the pod's waiting queue size.
 // the less waiting queue size the pod has, the higher score it will get (since it's more available to serve new request).
 type QueueScorer struct {
-	name string
+	tn plugins.TypedName
 }
 
-// Type returns the type of the scorer.
-func (s *QueueScorer) Type() string {
-	return QueueScorerType
-}
-
-// Name returns the name of the scorer.
-func (s *QueueScorer) Name() string {
-	return s.name
+// TypedName returns the type and name tuple of this plugin instance.
+func (s *QueueScorer) TypedName() plugins.TypedName {
+	return s.tn
 }
 
 // WithName sets the name of the scorer.
 func (s *QueueScorer) WithName(name string) *QueueScorer {
-	s.name = name
+	s.tn.Name = name
 	return s
 }
 
diff --git a/pkg/epp/scheduling/framework/scheduler_profile.go b/pkg/epp/scheduling/framework/scheduler_profile.go
index f41a915..307f474 100644
--- a/pkg/epp/scheduling/framework/scheduler_profile.go
+++ b/pkg/epp/scheduling/framework/scheduler_profile.go
@@ -86,14 +86,14 @@ func (p *SchedulerProfile) AddPlugins(pluginObjects ...plugins.Plugin) error {
 			p.scorers = append(p.scorers, weightedScorer)
 			plugin = weightedScorer.Scorer // if we got WeightedScorer, unwrap the plugin
 		} else if scorer, ok := plugin.(Scorer); ok { // if we got a Scorer instead of WeightedScorer that's an error.
-			return fmt.Errorf("failed to register scorer '%s' without a weight. follow function documentation to register a scorer", scorer.Type())
+			return fmt.Errorf("failed to register scorer '%s' without a weight. follow function documentation to register a scorer", scorer.TypedName())
 		}
 		if filter, ok := plugin.(Filter); ok {
 			p.filters = append(p.filters, filter)
 		}
 		if picker, ok := plugin.(Picker); ok {
 			if p.picker != nil {
-				return fmt.Errorf("failed to set '%s' as picker, already have a registered picker plugin '%s'", picker.Type(), p.picker.Type())
+				return fmt.Errorf("failed to set '%s' as picker, already have a registered picker plugin '%s'", picker.TypedName(), p.picker.TypedName())
 			}
 			p.picker = picker
 		}
@@ -127,11 +127,11 @@ func (p *SchedulerProfile) runFilterPlugins(ctx context.Context, request *types.
 	loggerDebug.Info("Before running filter plugins", "pods", filteredPods)
 
 	for _, filter := range p.filters {
-		loggerDebug.Info("Running filter plugin", "plugin", filter.Type())
+		loggerDebug.Info("Running filter plugin", "plugin", filter.TypedName().Type)
 		before := time.Now()
 		filteredPods = filter.Filter(ctx, cycleState, request, filteredPods)
-		metrics.RecordSchedulerPluginProcessingLatency(FilterPluginType, filter.Type(), time.Since(before))
-		loggerDebug.Info("Filter plugin result", "plugin", filter.Type(), "pods", filteredPods)
+		metrics.RecordSchedulerPluginProcessingLatency(FilterPluginType, filter.TypedName().Type, time.Since(before))
+		loggerDebug.Info("Filter plugin result", "plugin", filter.TypedName().Type, "pods", filteredPods)
 		if len(filteredPods) == 0 {
 			break
 		}
@@ -151,14 +151,14 @@ func (p *SchedulerProfile) runScorerPlugins(ctx context.Context, request *types.
 	}
 	// Iterate through each scorer in the chain and accumulate the weighted scores.
 	for _, scorer := range p.scorers {
-		loggerDebug.Info("Running scorer", "scorer", scorer.Type())
+		loggerDebug.Info("Running scorer", "scorer", scorer.TypedName().Type)
 		before := time.Now()
 		scores := scorer.Score(ctx, cycleState, request, pods)
-		metrics.RecordSchedulerPluginProcessingLatency(ScorerPluginType, scorer.Type(), time.Since(before))
+		metrics.RecordSchedulerPluginProcessingLatency(ScorerPluginType, scorer.TypedName().Type, time.Since(before))
 		for pod, score := range scores { // weight is relative to the sum of weights
 			weightedScorePerPod[pod] += score * float64(scorer.Weight())
 		}
-		loggerDebug.Info("After running scorer", "scorer", scorer.Type())
+		loggerDebug.Info("After running scorer", "scorer", scorer.TypedName().Type)
 	}
 	loggerDebug.Info("After running scorer plugins")
 
@@ -177,7 +177,7 @@ func (p *SchedulerProfile) runPickerPlugin(ctx context.Context, cycleState *type
 	loggerDebug.Info("Before running picker plugin", "pods weighted score", fmt.Sprint(weightedScorePerPod))
 	before := time.Now()
 	result := p.picker.Pick(ctx, cycleState, scoredPods)
-	metrics.RecordSchedulerPluginProcessingLatency(PickerPluginType, p.picker.Type(), time.Since(before))
+	metrics.RecordSchedulerPluginProcessingLatency(PickerPluginType, p.picker.TypedName().Type, time.Since(before))
 	loggerDebug.Info("After running picker plugin", "result", result)
 
 	return result
@@ -185,9 +185,9 @@ func (p *SchedulerProfile) runPickerPlugin(ctx context.Context, cycleState *type
 
 func (p *SchedulerProfile) runPostCyclePlugins(ctx context.Context, cycleState *types.CycleState, result *types.ProfileRunResult) {
 	for _, plugin := range p.postCyclePlugins {
-		log.FromContext(ctx).V(logutil.DEBUG).Info("Running post-cycle plugin", "plugin", plugin.Type())
+		log.FromContext(ctx).V(logutil.DEBUG).Info("Running post-cycle plugin", "plugin", plugin.TypedName().Type)
 		before := time.Now()
 		plugin.PostCycle(ctx, cycleState, result)
-		metrics.RecordSchedulerPluginProcessingLatency(PostCyclePluginType, plugin.Type(), time.Since(before))
+		metrics.RecordSchedulerPluginProcessingLatency(PostCyclePluginType, plugin.TypedName().Type, time.Since(before))
 	}
 }
diff --git a/pkg/epp/scheduling/scheduler.go b/pkg/epp/scheduling/scheduler.go
index f700057..d18e244 100644
--- a/pkg/epp/scheduling/scheduler.go
+++ b/pkg/epp/scheduling/scheduler.go
@@ -44,7 +44,6 @@ func NewScheduler() *Scheduler {
 	// it's possible to call NewSchedulerWithConfig to pass a different scheduler config.
 	// For build time plugins changes, it's recommended to call in main.go to NewSchedulerWithConfig.
 	loraAffinityFilter := filter.NewLoraAffinityFilter(config.Conf.LoraAffinityThreshold)
-	endpointSubsetFilter := filter.NewSubsetFilter()
 	leastQueueFilter := filter.NewLeastQueueFilter()
 	leastKvCacheFilter := filter.NewLeastKVCacheFilter()
 
@@ -71,8 +70,8 @@ func NewScheduler() *Scheduler {
 	}
 
 	defaultProfile := framework.NewSchedulerProfile().
-		WithFilters(endpointSubsetFilter, lowLatencyFilter).
-		WithPicker(&picker.RandomPicker{})
+		WithFilters(lowLatencyFilter).
+		WithPicker(picker.NewRandomPicker(picker.DefaultMaxNumOfEndpoints))
 
 	profileHandler := profile.NewSingleProfileHandler()
 
@@ -108,7 +107,7 @@ func (s *Scheduler) Schedule(ctx context.Context, request *types.LLMRequest, can
 	for { // get the next set of profiles to run iteratively based on the request and the previous execution results
 		before := time.Now()
 		profiles := s.profileHandler.Pick(ctx, cycleState, request, s.profiles, profileRunResults)
-		metrics.RecordSchedulerPluginProcessingLatency(framework.ProfilePickerType, s.profileHandler.Type(), time.Since(before))
+		metrics.RecordSchedulerPluginProcessingLatency(framework.ProfilePickerType, s.profileHandler.TypedName().Type, time.Since(before))
 		if len(profiles) == 0 { // profile picker didn't pick any profile to run
 			break
 		}
@@ -130,7 +129,7 @@ func (s *Scheduler) Schedule(ctx context.Context, request *types.LLMRequest, can
 
 	before := time.Now()
 	result, err := s.profileHandler.ProcessResults(ctx, cycleState, request, profileRunResults)
-	metrics.RecordSchedulerPluginProcessingLatency(framework.ProcessProfilesResultsType, s.profileHandler.Type(), time.Since(before))
+	metrics.RecordSchedulerPluginProcessingLatency(framework.ProcessProfilesResultsType, s.profileHandler.TypedName().Type, time.Since(before))
 
 	return result, err
 }
diff --git a/pkg/epp/scheduling/types/cycle_state.go b/pkg/epp/scheduling/types/cycle_state.go
index 97381dd..789ece2 100644
--- a/pkg/epp/scheduling/types/cycle_state.go
+++ b/pkg/epp/scheduling/types/cycle_state.go
@@ -59,7 +59,7 @@ func (c *CycleState) Clone() *CycleState {
 	}
 	copy := NewCycleState()
 	// Safe copy storage in case of overwriting.
-	c.storage.Range(func(k, v interface{}) bool {
+	c.storage.Range(func(k, v any) bool {
 		copy.storage.Store(k, v.(StateData).Clone())
 		return true
 	})
diff --git a/pkg/epp/scheduling/types/types.go b/pkg/epp/scheduling/types/types.go
index d587297..2962117 100644
--- a/pkg/epp/scheduling/types/types.go
+++ b/pkg/epp/scheduling/types/types.go
@@ -33,29 +33,12 @@ type LLMRequest struct {
 	Prompt string
 	// Headers is a map of the request headers.
 	Headers map[string]string
-
-	// metadata is a map of metadata in the request
-	metadata map[string]any
-}
-
-func NewLLMRequest(reqID, targetModel, prompt string, headers map[string]string, metadata map[string]any) *LLMRequest {
-	return &LLMRequest{
-		RequestId:   reqID,
-		TargetModel: targetModel,
-		Prompt:      prompt,
-		Headers:     headers,
-		metadata:    metadata,
-	}
 }
 
 func (r *LLMRequest) String() string {
 	return fmt.Sprintf("RequestID: %s, TargetModel: %s, PromptLength: %d, Headers: %v", r.RequestId, r.TargetModel, len(r.Prompt), r.Headers)
 }
 
-func (r *LLMRequest) GetMetadata() map[string]any {
-	return r.metadata
-}
-
 type Pod interface {
 	GetPod() *backend.Pod
 	GetMetrics() *backendmetrics.MetricsState
@@ -87,17 +70,9 @@ type PodMetrics struct {
 	*backendmetrics.MetricsState
 }
 
-func ToSchedulerPodMetrics(pods []backendmetrics.PodMetrics) []Pod {
-	pm := make([]Pod, 0, len(pods))
-	for _, pod := range pods {
-		pm = append(pm, &PodMetrics{Pod: pod.GetPod().Clone(), MetricsState: pod.GetMetrics().Clone()})
-	}
-	return pm
-}
-
 // ProfileRunResult captures the profile run result.
 type ProfileRunResult struct {
-	TargetPod Pod
+	TargetPods []Pod
 }
 
 // SchedulingResult captures the result of the scheduling cycle.
diff --git a/pkg/epp/server/runserver.go b/pkg/epp/server/runserver.go
index daee8e1..67dc78e 100644
--- a/pkg/epp/server/runserver.go
+++ b/pkg/epp/server/runserver.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"crypto/tls"
 	"fmt"
-
 	"time"
 
 	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
@@ -63,6 +62,8 @@ type ExtProcServerRunner struct {
 // Default values for CLI flags in main
 const (
 	DefaultGrpcPort                                 = 9002                             // default for --grpcPort
+	DefaultGrpcHealthPort                           = 9003                             // default for --grpcHealthPort
+	DefaultMetricsPort                              = 9090                             // default for --metricsPort
 	DefaultDestinationEndpointHintMetadataNamespace = "envoy.lb"                       // default for --destinationEndpointHintMetadataNamespace
 	DefaultDestinationEndpointHintKey               = "x-gateway-destination-endpoint" // default for --destinationEndpointHintKey
 	DefaultPoolName                                 = ""                               // required but no default
@@ -71,6 +72,12 @@ const (
 	DefaultRefreshPrometheusMetricsInterval         = 5 * time.Second                  // default for --refreshPrometheusMetricsInterval
 	DefaultSecureServing                            = true                             // default for --secureServing
 	DefaultHealthChecking                           = false                            // default for --healthChecking
+	DefaultTotalQueuedRequestsMetric                = "vllm:num_requests_waiting"      // default for --totalQueuedRequestsMetric
+	DefaultKvCacheUsagePercentageMetric             = "vllm:gpu_cache_usage_perc"      // default for --kvCacheUsagePercentageMetric
+	DefaultLoraInfoMetric                           = "vllm:lora_requests_info"        // default for --loraInfoMetric
+	DefaultCertPath                                 = ""                               // default for --certPath
+	DefaultConfigFile                               = ""                               // default for --configFile
+	DefaultConfigText                               = ""                               // default for --configText
 )
 
 // NewDefaultExtProcServerRunner creates a runner with default values.
@@ -93,7 +100,7 @@ func (r *ExtProcServerRunner) SetupWithManager(ctx context.Context, mgr ctrl.Man
 	// Create the controllers and register them with the manager
 	if err := (&controller.InferencePoolReconciler{
 		Datastore: r.Datastore,
-		Client:    mgr.GetClient(),
+		Reader:    mgr.GetClient(),
 		Record:    mgr.GetEventRecorderFor("InferencePool"),
 	}).SetupWithManager(mgr); err != nil {
 		return fmt.Errorf("failed setting up InferencePoolReconciler: %w", err)
@@ -101,7 +108,7 @@ func (r *ExtProcServerRunner) SetupWithManager(ctx context.Context, mgr ctrl.Man
 
 	if err := (&controller.InferenceModelReconciler{
 		Datastore:          r.Datastore,
-		Client:             mgr.GetClient(),
+		Reader:             mgr.GetClient(),
 		PoolNamespacedName: r.PoolNamespacedName,
 		Record:             mgr.GetEventRecorderFor("InferenceModel"),
 	}).SetupWithManager(ctx, mgr); err != nil {
@@ -110,7 +117,7 @@ func (r *ExtProcServerRunner) SetupWithManager(ctx context.Context, mgr ctrl.Man
 
 	if err := (&controller.PodReconciler{
 		Datastore: r.Datastore,
-		Client:    mgr.GetClient(),
+		Reader:    mgr.GetClient(),
 		Record:    mgr.GetEventRecorderFor("pod"),
 	}).SetupWithManager(mgr); err != nil {
 		return fmt.Errorf("failed setting up PodReconciler: %v", err)
diff --git a/pkg/epp/util/error/error.go b/pkg/epp/util/error/error.go
index d580d66..2648309 100644
--- a/pkg/epp/util/error/error.go
+++ b/pkg/epp/util/error/error.go
@@ -30,6 +30,7 @@ const (
 	Unknown                        = "Unknown"
 	BadRequest                     = "BadRequest"
 	Internal                       = "Internal"
+	ServiceUnavailable             = "ServiceUnavailable"
 	ModelServerError               = "ModelServerError"
 	BadConfiguration               = "BadConfiguration"
 	InferencePoolResourceExhausted = "InferencePoolResourceExhausted"
diff --git a/pkg/epp/util/logging/fatal.go b/pkg/epp/util/logging/fatal.go
index d8a9a93..ddc15c4 100644
--- a/pkg/epp/util/logging/fatal.go
+++ b/pkg/epp/util/logging/fatal.go
@@ -25,7 +25,7 @@ import (
 // Fatal calls logger.Error followed by os.Exit(1).
 //
 // This is a utility function and should not be used in production code!
-func Fatal(logger logr.Logger, err error, msg string, keysAndValues ...interface{}) {
+func Fatal(logger logr.Logger, err error, msg string, keysAndValues ...any) {
 	logger.Error(err, msg, keysAndValues...)
 	os.Exit(1)
 }
diff --git a/pkg/epp/util/request/body.go b/pkg/epp/util/request/body.go
index 83a600f..46de1fa 100644
--- a/pkg/epp/util/request/body.go
+++ b/pkg/epp/util/request/body.go
@@ -22,14 +22,14 @@ import (
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
 )
 
-func ExtractPromptFromRequestBody(body map[string]interface{}) (string, error) {
+func ExtractPromptFromRequestBody(body map[string]any) (string, error) {
 	if _, ok := body["messages"]; ok {
 		return extractPromptFromMessagesField(body)
 	}
 	return extractPromptField(body)
 }
 
-func extractPromptField(body map[string]interface{}) (string, error) {
+func extractPromptField(body map[string]any) (string, error) {
 	prompt, ok := body["prompt"]
 	if !ok {
 		return "", errutil.Error{Code: errutil.BadRequest, Msg: "prompt not found in request"}
@@ -41,12 +41,12 @@ func extractPromptField(body map[string]interface{}) (string, error) {
 	return promptStr, nil
 }
 
-func extractPromptFromMessagesField(body map[string]interface{}) (string, error) {
+func extractPromptFromMessagesField(body map[string]any) (string, error) {
 	messages, ok := body["messages"]
 	if !ok {
 		return "", errutil.Error{Code: errutil.BadRequest, Msg: "messages not found in request"}
 	}
-	messageList, ok := messages.([]interface{})
+	messageList, ok := messages.([]any)
 	if !ok {
 		return "", errutil.Error{Code: errutil.BadRequest, Msg: "messages is not a list"}
 	}
@@ -56,7 +56,7 @@ func extractPromptFromMessagesField(body map[string]interface{}) (string, error)
 
 	prompt := ""
 	for _, msg := range messageList {
-		msgMap, ok := msg.(map[string]interface{})
+		msgMap, ok := msg.(map[string]any)
 		if !ok {
 			continue
 		}
diff --git a/site-src/guides/index.md b/site-src/guides/index.md
index ac80149..38b631d 100644
--- a/site-src/guides/index.md
+++ b/site-src/guides/index.md
@@ -150,7 +150,7 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
       2. Install Istio
 
          ```
-         TAG=1.26-alpha.665da00e1e5392c31cf44cd4dedecd354dd660d5
+         TAG=1.27-alpha.0551127f00634403cddd4634567e65a8ecc499a7
          # on Linux
          wget https://storage.googleapis.com/istio-build/dev/$TAG/istioctl-$TAG-linux-amd64.tar.gz
          tar -xvf istioctl-$TAG-linux-amd64.tar.gz
@@ -215,7 +215,7 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
       2. Set the Kgateway version and install the Kgateway CRDs.
 
          ```bash
-         KGTW_VERSION=v2.0.2
+         KGTW_VERSION=v2.0.3
          helm upgrade -i --create-namespace --namespace kgateway-system --version $KGTW_VERSION kgateway-crds oci://cr.kgateway.dev/kgateway-dev/charts/kgateway-crds
          ```
 



Test patch:
diff --git a/conformance/testing-epp/plugins/filter/request_header_based_filter.go b/conformance/testing-epp/plugins/filter/request_header_based_filter.go
index ee19111..75039d9 100644
--- a/conformance/testing-epp/plugins/filter/request_header_based_filter.go
+++ b/conformance/testing-epp/plugins/filter/request_header_based_filter.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"strings"
 
+	"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"
 )
@@ -38,20 +39,19 @@ var _ framework.Filter = &HeaderBasedTestingFilter{}
 // NewHeaderBasedTestingFilter initializes a new HeaderBasedTestingFilter.
 // This should only be used for testing purposes.
 func NewHeaderBasedTestingFilter() *HeaderBasedTestingFilter {
-	return &HeaderBasedTestingFilter{}
+	return &HeaderBasedTestingFilter{
+		tn: plugins.TypedName{Type: "header-based-testing", Name: "header-based-testing-filter"},
+	}
 }
 
 // HeaderBasedTestingFilter filters Pods based on an address specified in the "test-epp-endpoint-selection" request header.
-type HeaderBasedTestingFilter struct{}
-
-// Type returns the type of the filter.
-func (f *HeaderBasedTestingFilter) Type() string {
-	return "header-based-testing"
+type HeaderBasedTestingFilter struct {
+	tn plugins.TypedName
 }
 
-// Name returns the type of the filter.
-func (f *HeaderBasedTestingFilter) Name() string {
-	return "header-based-testing-filter"
+// TypedName returns the type and name tuple of this plugin instance.
+func (f *HeaderBasedTestingFilter) TypedName() plugins.TypedName {
+	return f.tn
 }
 
 // Filter selects pods that match the IP addresses specified in the request header.
diff --git a/conformance/testing-epp/scheduler.go b/conformance/testing-epp/scheduler.go
index 94f9ee5..aaee956 100644
--- a/conformance/testing-epp/scheduler.go
+++ b/conformance/testing-epp/scheduler.go
@@ -30,7 +30,7 @@ import (
 func NewReqHeaderBasedScheduler() *scheduling.Scheduler {
 	predicatableSchedulerProfile := framework.NewSchedulerProfile().
 		WithFilters(filter.NewHeaderBasedTestingFilter()).
-		WithPicker(picker.NewMaxScorePicker())
+		WithPicker(picker.NewMaxScorePicker(picker.DefaultMaxNumOfEndpoints))
 
 	return scheduling.NewSchedulerWithConfig(scheduling.NewSchedulerConfig(
 		profile.NewSingleProfileHandler(), map[string]*framework.SchedulerProfile{"req-header-based-profile": predicatableSchedulerProfile}))
diff --git a/conformance/testing-epp/sheduler_test.go b/conformance/testing-epp/scheduler_test.go
similarity index 89%
rename from conformance/testing-epp/sheduler_test.go
rename to conformance/testing-epp/scheduler_test.go
index 4901e03..95d627e 100644
--- a/conformance/testing-epp/sheduler_test.go
+++ b/conformance/testing-epp/scheduler_test.go
@@ -31,7 +31,7 @@ import (
 func TestSchedule(t *testing.T) {
 	tests := []struct {
 		name    string
-		input   []backendmetrics.PodMetrics
+		input   []types.Pod
 		req     *types.LLMRequest
 		wantRes *types.SchedulingResult
 		err     bool
@@ -47,7 +47,7 @@ func TestSchedule(t *testing.T) {
 		},
 		{
 			name: "req header not set",
-			input: []backendmetrics.PodMetrics{
+			input: []types.Pod{
 				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "random-endpoint"}},
 			},
 			req: &types.LLMRequest{
@@ -59,7 +59,7 @@ func TestSchedule(t *testing.T) {
 		},
 		{
 			name: "no pods address from the candidate pods matches req header address",
-			input: []backendmetrics.PodMetrics{
+			input: []types.Pod{
 				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "nonmatched-endpoint"}},
 			},
 			req: &types.LLMRequest{
@@ -71,7 +71,7 @@ func TestSchedule(t *testing.T) {
 		},
 		{
 			name: "one pod address from the candidate pods matches req header address",
-			input: []backendmetrics.PodMetrics{
+			input: []types.Pod{
 				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "nonmatched-endpoint"}},
 				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{Address: "matched-endpoint"}},
 			},
@@ -82,11 +82,13 @@ func TestSchedule(t *testing.T) {
 			wantRes: &types.SchedulingResult{
 				ProfileResults: map[string]*types.ProfileRunResult{
 					"req-header-based-profile": {
-						TargetPod: &types.ScoredPod{
-							Pod: &types.PodMetrics{
-								Pod: &backend.Pod{
-									Address: "matched-endpoint",
-									Labels:  map[string]string{},
+						TargetPods: []types.Pod{
+							&types.ScoredPod{
+								Pod: &types.PodMetrics{
+									Pod: &backend.Pod{
+										Address: "matched-endpoint",
+										Labels:  map[string]string{},
+									},
 								},
 							},
 						},
@@ -100,7 +102,7 @@ func TestSchedule(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			scheduler := NewReqHeaderBasedScheduler()
-			got, err := scheduler.Schedule(context.Background(), test.req, types.ToSchedulerPodMetrics(test.input))
+			got, err := scheduler.Schedule(context.Background(), test.req, test.input)
 			if test.err != (err != nil) {
 				t.Errorf("Unexpected error, got %v, want %v", err, test.err)
 			}
diff --git a/conformance/tests/basic/inferencepool_invalid_epp_service.go b/conformance/tests/basic/inferencepool_invalid_epp_service.go
index 73e6dc0..a9c3059 100644
--- a/conformance/tests/basic/inferencepool_invalid_epp_service.go
+++ b/conformance/tests/basic/inferencepool_invalid_epp_service.go
@@ -17,18 +17,19 @@ limitations under the License.
 package basic
 
 import (
-	"net/http"
 	"testing"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	inferenceapi "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
 	"sigs.k8s.io/gateway-api/conformance/utils/suite"
 	"sigs.k8s.io/gateway-api/pkg/features"
 
 	"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
-	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
-	conformancehttp "sigs.k8s.io/gateway-api/conformance/utils/http"
+	k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes"
+	trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic"
 )
 
 func init() {
@@ -49,30 +50,33 @@ var InferencePoolInvalidEPPService = suite.ConformanceTest{
 			routePath      = "/invalid-epp-test"
 			infraNamespace = "gateway-conformance-infra"
 			appNamespace   = "gateway-conformance-app-backend"
+			poolName       = "pool-with-invalid-epp"
 		)
 
 		routeNN := types.NamespacedName{Name: "httproute-for-invalid-epp-pool", Namespace: appNamespace}
 		gwNN := types.NamespacedName{Name: "conformance-primary-gateway", Namespace: infraNamespace}
+		poolNN := types.NamespacedName{Name: poolName, Namespace: appNamespace}
 
-		gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, s.Client, s.TimeoutConfig, s.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
-
-		t.Run("HTTPRoute has a ResolvedRefs Condition with status False and Reason BackendNotFound", func(t *testing.T) {
-			resolvedRefsCond := metav1.Condition{
-				Type:   string(gatewayv1.RouteConditionResolvedRefs),
+		gwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gwNN)
+		acceptedCondition := metav1.Condition{
+			Type:   string(gatewayv1.RouteConditionAccepted),
+			Status: metav1.ConditionTrue,
+			Reason: string(gatewayv1.RouteReasonAccepted),
+		}
+		kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, acceptedCondition)
+		t.Run("InferecePool has a ResolvedRefs Condition with status False", func(t *testing.T) {
+			acceptedCondition := metav1.Condition{
+				Type:   string(inferenceapi.InferencePoolConditionResolvedRefs), // Standard condition type
 				Status: metav1.ConditionFalse,
-				Reason: string(gatewayv1.RouteReasonBackendNotFound),
+				Reason: "", // "" means we don't strictly check the Reason for this basic test.
 			}
-			kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, resolvedRefsCond)
+			k8sutils.InferencePoolMustHaveCondition(t, s.Client, poolNN, acceptedCondition)
 		})
 
 		t.Run("Request to a route with an invalid backend reference receives a 500 response", func(t *testing.T) {
-			conformancehttp.MakeRequestAndExpectEventuallyConsistentResponse(t, s.RoundTripper, s.TimeoutConfig, gwAddr, conformancehttp.ExpectedResponse{
-				Request: conformancehttp.Request{
-					Path: routePath,
-				},
-				Response: conformancehttp.Response{
-					StatusCode: http.StatusInternalServerError,
-				},
+			trafficutils.MakeRequestAndExpectEventuallyConsistentResponse(t, s.RoundTripper, s.TimeoutConfig, gwAddr, trafficutils.Request{
+				Path:               routePath,
+				ExpectedStatusCode: 5, // Expecting response status code 5XX.
 			})
 		})
 	},
diff --git a/conformance/tests/basic/inferencepool_invalid_epp_service.yaml b/conformance/tests/basic/inferencepool_invalid_epp_service.yaml
index 3a19036..7d9295f 100644
--- a/conformance/tests/basic/inferencepool_invalid_epp_service.yaml
+++ b/conformance/tests/basic/inferencepool_invalid_epp_service.yaml
@@ -5,7 +5,7 @@ metadata:
   namespace: gateway-conformance-app-backend
 spec:
   selector:
-    app: "inference-model-1"
+    app: primary-inference-model-server
   targetPortNumber: 3000
   extensionRef:
     name: non-existent-epp-svc
diff --git a/pkg/epp/backend/metrics/metrics_test.go b/pkg/epp/backend/metrics/metrics_test.go
index bfc3e01..9f7c2b8 100644
--- a/pkg/epp/backend/metrics/metrics_test.go
+++ b/pkg/epp/backend/metrics/metrics_test.go
@@ -495,9 +495,9 @@ func TestFetchMetrics(t *testing.T) {
 		},
 	}
 	existing := &MetricsState{}
-	p := &PodMetricsClientImpl{} // No MetricMapping needed for this basic test
+	p := &PodMetricsClientImpl{ModelServerMetricsPort: 9999, ModelServerMetricsPath: "/metrics"} // No MetricMapping needed for this basic test
 
-	_, err := p.FetchMetrics(ctx, pod, existing, 9999) // Use a port that's unlikely to be in use.
+	_, err := p.FetchMetrics(ctx, pod, existing, 9999) // Use a port that's unlikely to be in use
 	if err == nil {
 		t.Errorf("FetchMetrics() expected error, got nil")
 	}
diff --git a/pkg/epp/common/config/loader/configloader_test.go b/pkg/epp/common/config/loader/configloader_test.go
index a6a9820..a0172c0 100644
--- a/pkg/epp/common/config/loader/configloader_test.go
+++ b/pkg/epp/common/config/loader/configloader_test.go
@@ -19,10 +19,12 @@ package loader
 import (
 	"context"
 	"encoding/json"
+	"os"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
 
 	configapi "sigs.k8s.io/gateway-api-inference-extension/api/config/v1alpha1"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
@@ -43,8 +45,6 @@ const (
 )
 
 func TestLoadConfiguration(t *testing.T) {
-	test2Weight := 50
-
 	registerTestPlugins()
 
 	goodConfig := &configapi.EndpointPickerConfig{
@@ -81,7 +81,7 @@ func TestLoadConfiguration(t *testing.T) {
 					},
 					{
 						PluginRef: "test-two",
-						Weight:    &test2Weight,
+						Weight:    ptr.To(50),
 					},
 					{
 						PluginRef: "testPicker",
@@ -178,19 +178,18 @@ func TestLoadConfiguration(t *testing.T) {
 			want:       goodConfig,
 			wantErr:    false,
 		},
-		{
-			name:       "noSuchFile",
-			configText: "",
-			configFile: "../../../../../test/testdata/configloader_error_test.yaml",
-			wantErr:    true,
-		},
 	}
 
 	for _, test := range tests {
-		got, err := LoadConfig([]byte(test.configText), test.configFile)
+		configBytes := []byte(test.configText)
+		if test.configFile != "" {
+			configBytes, _ = os.ReadFile(test.configFile)
+		}
+
+		got, err := LoadConfig(configBytes, utils.NewTestHandle(context.Background()))
 		if err != nil {
 			if !test.wantErr {
-				t.Fatalf("In test %s LoadConfig returned unexpected error: %v, want %v", test.name, err, test.wantErr)
+				t.Fatalf("In test '%s' LoadConfig returned unexpected error: %v, want %v", test.name, err, test.wantErr)
 			}
 			t.Logf("error was %s", err)
 		} else {
@@ -204,41 +203,25 @@ func TestLoadConfiguration(t *testing.T) {
 	}
 }
 
-func TestLoadPluginReferences(t *testing.T) {
-	ctx := context.Background()
-	theConfig, err := LoadConfig([]byte(successConfigText), "")
+func TestInstantiatePlugins(t *testing.T) {
+	handle := utils.NewTestHandle(context.Background())
+	_, err := LoadConfig([]byte(successConfigText), handle)
 	if err != nil {
-		t.Fatalf("LoadConfig returned unexpected error: %v", err)
+		t.Fatalf("LoadConfig returned unexpected error - %v", err)
 	}
-	handle := utils.NewTestHandle(ctx)
-	err = LoadPluginReferences(theConfig.Plugins, handle)
-	if err != nil {
-		t.Fatalf("LoadPluginReferences returned unexpected error: %v", err)
-	}
-	if len(handle.Plugins().GetAllPlugins()) == 0 {
-		t.Fatalf("LoadPluginReferences returned an empty set of references")
+	if len(handle.GetAllPlugins()) == 0 {
+		t.Fatalf("unexpected empty set of loaded plugins")
 	}
-	if t1 := handle.Plugins().Plugin("test1"); t1 == nil {
-		t.Fatalf("LoadPluginReferences returned references did not contain test1")
+	if t1 := handle.Plugin("test1"); t1 == nil {
+		t.Fatalf("loaded plugins did not contain test1")
 	} else if _, ok := t1.(*test1); !ok {
-		t.Fatalf("LoadPluginReferences returned references value for test1 has the wrong type %#v", t1)
-	}
-
-	theConfig, err = LoadConfig([]byte(errorBadPluginReferenceParametersText), "")
-	if err != nil {
-		t.Fatalf("LoadConfig returned unexpected error: %v", err)
-	}
-	err = LoadPluginReferences(theConfig.Plugins, utils.NewTestHandle(ctx))
-	if err == nil {
-		t.Fatalf("LoadPluginReferences did not return the expected error")
+		t.Fatalf("loaded plugins returned test1 has the wrong type %#v", t1)
 	}
-}
 
-func TestInstantiatePlugin(t *testing.T) {
-	plugSpec := configapi.PluginSpec{Type: "plover"}
-	_, err := instantiatePlugin(plugSpec, utils.NewTestHandle(context.Background()))
+	handle = utils.NewTestHandle(context.Background())
+	_, err = LoadConfig([]byte(errorBadPluginReferenceParametersText), handle)
 	if err == nil {
-		t.Fatalf("InstantiatePlugin did not return the expected error")
+		t.Fatalf("LoadConfig did not return error as expected ")
 	}
 }
 
@@ -287,26 +270,17 @@ func TestLoadSchedulerConfig(t *testing.T) {
 
 	registerNeededPlgugins()
 
-	ctx := context.Background()
-
 	for _, test := range tests {
-		theConfig, err := LoadConfig([]byte(test.configText), "")
+		handle := utils.NewTestHandle(context.Background())
+		config, err := LoadConfig([]byte(test.configText), handle)
 		if err != nil {
 			if test.wantErr {
 				continue
 			}
 			t.Fatalf("LoadConfig returned unexpected error: %v", err)
 		}
-		handle := utils.NewTestHandle(ctx)
-		err = LoadPluginReferences(theConfig.Plugins, handle)
-		if err != nil {
-			if test.wantErr {
-				continue
-			}
-			t.Fatalf("LoadPluginReferences returned unexpected error: %v", err)
-		}
 
-		_, err = LoadSchedulerConfig(theConfig.SchedulingProfiles, handle)
+		_, err = LoadSchedulerConfig(config.SchedulingProfiles, handle)
 		if err != nil {
 			if !test.wantErr {
 				t.Errorf("LoadSchedulerConfig returned an unexpected error. error %v", err)
@@ -555,15 +529,18 @@ schedulingProfiles:
 var _ framework.Filter = &test1{}
 
 type test1 struct {
+	typedName plugins.TypedName
 	Threshold int `json:"threshold"`
 }
 
-func (f *test1) Type() string {
-	return test1Type
+func newTest1() *test1 {
+	return &test1{
+		typedName: plugins.TypedName{Type: test1Type, Name: "test-1"},
+	}
 }
 
-func (f *test1) Name() string {
-	return "test-1"
+func (f *test1) TypedName() plugins.TypedName {
+	return f.typedName
 }
 
 // Filter filters out pods that doesn't meet the filter criteria.
@@ -575,14 +552,18 @@ func (f *test1) Filter(_ context.Context, _ *types.CycleState, _ *types.LLMReque
 var _ framework.Scorer = &test2{}
 var _ framework.PostCycle = &test2{}
 
-type test2 struct{}
+type test2 struct {
+	typedName plugins.TypedName
+}
 
-func (f *test2) Type() string {
-	return test2Type
+func newTest2() *test2 {
+	return &test2{
+		typedName: plugins.TypedName{Type: test2Type, Name: "test-2"},
+	}
 }
 
-func (f *test2) Name() string {
-	return "test-2"
+func (m *test2) TypedName() plugins.TypedName {
+	return m.typedName
 }
 
 func (m *test2) Score(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, _ []types.Pod) map[types.Pod]float64 {
@@ -594,14 +575,18 @@ func (m *test2) PostCycle(_ context.Context, _ *types.CycleState, _ *types.Profi
 // compile-time type validation
 var _ framework.Picker = &testPicker{}
 
-type testPicker struct{}
+type testPicker struct {
+	typedName plugins.TypedName
+}
 
-func (p *testPicker) Type() string {
-	return testPickerType
+func newTestPicker() *testPicker {
+	return &testPicker{
+		typedName: plugins.TypedName{Type: testPickerType, Name: "test-picker"},
+	}
 }
 
-func (p *testPicker) Name() string {
-	return "test-picker"
+func (p *testPicker) TypedName() plugins.TypedName {
+	return p.typedName
 }
 
 func (p *testPicker) Pick(_ context.Context, _ *types.CycleState, _ []*types.ScoredPod) *types.ProfileRunResult {
@@ -611,14 +596,18 @@ func (p *testPicker) Pick(_ context.Context, _ *types.CycleState, _ []*types.Sco
 // compile-time type validation
 var _ framework.ProfileHandler = &testProfileHandler{}
 
-type testProfileHandler struct{}
+type testProfileHandler struct {
+	typedName plugins.TypedName
+}
 
-func (p *testProfileHandler) Type() string {
-	return testProfileHandlerType
+func newTestProfileHandler() *testProfileHandler {
+	return &testProfileHandler{
+		typedName: plugins.TypedName{Type: testProfileHandlerType, Name: "test-profile-handler"},
+	}
 }
 
-func (p *testProfileHandler) Name() string {
-	return "test-profile-handler"
+func (p *testProfileHandler) TypedName() plugins.TypedName {
+	return p.typedName
 }
 
 func (p *testProfileHandler) Pick(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, _ map[string]*framework.SchedulerProfile, _ map[string]*types.ProfileRunResult) map[string]*framework.SchedulerProfile {
@@ -631,28 +620,28 @@ func (p *testProfileHandler) ProcessResults(_ context.Context, _ *types.CycleSta
 
 func registerTestPlugins() {
 	plugins.Register(test1Type,
-		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
-			result := test1{}
-			err := json.Unmarshal(parameters, &result)
-			return &result, err
+		func(_ string, parameters json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+			result := newTest1()
+			err := json.Unmarshal(parameters, result)
+			return result, err
 		},
 	)
 
 	plugins.Register(test2Type,
-		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
-			return &test2{}, nil
+		func(_ string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+			return newTest2(), nil
 		},
 	)
 
 	plugins.Register(testPickerType,
-		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
-			return &testPicker{}, nil
+		func(_ string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+			return newTestPicker(), nil
 		},
 	)
 
 	plugins.Register(testProfileHandlerType,
-		func(name string, parameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
-			return &testProfileHandler{}, nil
+		func(_ string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+			return newTestProfileHandler(), nil
 		},
 	)
 }
diff --git a/pkg/epp/controller/inferencemodel_reconciler_test.go b/pkg/epp/controller/inferencemodel_reconciler_test.go
index 80c30e1..838737a 100644
--- a/pkg/epp/controller/inferencemodel_reconciler_test.go
+++ b/pkg/epp/controller/inferencemodel_reconciler_test.go
@@ -201,7 +201,7 @@ func TestInferenceModelReconciler(t *testing.T) {
 			}
 			_ = ds.PoolSet(context.Background(), fakeClient, pool)
 			reconciler := &InferenceModelReconciler{
-				Client:             fakeClient,
+				Reader:             fakeClient,
 				Record:             record.NewFakeRecorder(10),
 				Datastore:          ds,
 				PoolNamespacedName: types.NamespacedName{Name: pool.Name, Namespace: pool.Namespace},
diff --git a/pkg/epp/controller/inferencepool_reconciler_test.go b/pkg/epp/controller/inferencepool_reconciler_test.go
index b7e2833..c61abb3 100644
--- a/pkg/epp/controller/inferencepool_reconciler_test.go
+++ b/pkg/epp/controller/inferencepool_reconciler_test.go
@@ -96,7 +96,7 @@ func TestInferencePoolReconciler(t *testing.T) {
 
 	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 	datastore := datastore.NewDatastore(ctx, pmf)
-	inferencePoolReconciler := &InferencePoolReconciler{Client: fakeClient, Datastore: datastore}
+	inferencePoolReconciler := &InferencePoolReconciler{Reader: fakeClient, Datastore: datastore}
 
 	// Step 1: Inception, only ready pods matching pool1 are added to the store.
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
diff --git a/pkg/epp/controller/pod_reconciler_test.go b/pkg/epp/controller/pod_reconciler_test.go
index d2bdd5d..bbb2a83 100644
--- a/pkg/epp/controller/pod_reconciler_test.go
+++ b/pkg/epp/controller/pod_reconciler_test.go
@@ -187,7 +187,7 @@ func TestPodReconciler(t *testing.T) {
 				store.PodUpdateOrAddIfNotExist(pod)
 			}
 
-			podReconciler := &PodReconciler{Client: fakeClient, Datastore: store}
+			podReconciler := &PodReconciler{Reader: fakeClient, Datastore: store}
 			if test.req == nil {
 				namespacedName := types.NamespacedName{Name: test.incomingPod.Name, Namespace: test.incomingPod.Namespace}
 				test.req = &ctrl.Request{NamespacedName: namespacedName}
diff --git a/pkg/epp/handlers/response_test.go b/pkg/epp/handlers/response_test.go
index bfe5a62..b79f4ee 100644
--- a/pkg/epp/handlers/response_test.go
+++ b/pkg/epp/handlers/response_test.go
@@ -86,7 +86,7 @@ func TestHandleResponseBody(t *testing.T) {
 			if reqCtx == nil {
 				reqCtx = &RequestContext{}
 			}
-			var responseMap map[string]interface{}
+			var responseMap map[string]any
 			marshalErr := json.Unmarshal(test.body, &responseMap)
 			if marshalErr != nil {
 				t.Error(marshalErr, "Error unmarshaling request body")
diff --git a/pkg/epp/requestcontrol/director_test.go b/pkg/epp/requestcontrol/director_test.go
index aa39666..d0571bf 100644
--- a/pkg/epp/requestcontrol/director_test.go
+++ b/pkg/epp/requestcontrol/director_test.go
@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -36,6 +37,7 @@ import (
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
@@ -129,11 +131,13 @@ func TestDirector_HandleRequest(t *testing.T) {
 	defaultSuccessfulScheduleResults := &schedulingtypes.SchedulingResult{
 		ProfileResults: map[string]*schedulingtypes.ProfileRunResult{
 			"testProfile": {
-				TargetPod: &schedulingtypes.ScoredPod{
-					Pod: &schedulingtypes.PodMetrics{
-						Pod: &backend.Pod{
-							Address:        "192.168.1.100",
-							NamespacedName: k8stypes.NamespacedName{Name: "pod1", Namespace: "default"},
+				TargetPods: []schedulingtypes.Pod{
+					&schedulingtypes.ScoredPod{
+						Pod: &schedulingtypes.PodMetrics{
+							Pod: &backend.Pod{
+								Address:        "192.168.1.100",
+								NamespacedName: k8stypes.NamespacedName{Name: "pod1", Namespace: "default"},
+							},
 						},
 					},
 				},
@@ -144,7 +148,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 
 	tests := []struct {
 		name                   string
-		reqBodyMap             map[string]interface{}
+		reqBodyMap             map[string]any
 		mockSaturationDetector *mockSaturationDetector
 		schedulerMockSetup     func(m *mockScheduler)
 		wantErrCode            string                   // Expected errutil code string
@@ -153,7 +157,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 	}{
 		{
 			name: "successful completions request (critical, saturation ignored)",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  model,
 				"prompt": "critical prompt",
 			},
@@ -174,10 +178,10 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 		{
 			name: "successful chat completions request (critical, saturation ignored)",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model": model,
-				"messages": []interface{}{
-					map[string]interface{}{
+				"messages": []any{
+					map[string]any{
 						"role":    "user",
 						"content": "critical prompt",
 					},
@@ -199,14 +203,14 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 		{
 			name: "successful chat completions request with multiple messages (critical, saturation ignored)",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model": model,
-				"messages": []interface{}{
-					map[string]interface{}{
+				"messages": []any{
+					map[string]any{
 						"role":    "developer",
 						"content": "You are a helpful assistant.",
 					},
-					map[string]interface{}{
+					map[string]any{
 						"role":    "user",
 						"content": "Hello!",
 					},
@@ -228,7 +232,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 		{
 			name: "successful completions request (sheddable, not saturated)",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  modelSheddable,
 				"prompt": "sheddable prompt",
 			},
@@ -249,7 +253,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 		{
 			name: "successful request with target model resolution",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  modelWithResolvedTarget,
 				"prompt": "prompt for target resolution",
 			},
@@ -283,7 +287,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 				TargetEndpoint: "192.168.1.100:8000",
 			},
 			wantMutatedBodyModel: "food-review-1",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  "food-review-1",
 				"prompt": "test prompt",
 			},
@@ -292,7 +296,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 		{
 
 			name: "request dropped (sheddable, saturated)",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  modelSheddable,
 				"prompt": "sheddable prompt",
 			},
@@ -301,27 +305,27 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 		{
 			name:                   "model not found, expect err",
-			reqBodyMap:             map[string]interface{}{"prompt": "p"},
+			reqBodyMap:             map[string]any{"prompt": "p"},
 			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
 			wantErrCode:            errutil.BadRequest,
 		},
 
 		{
 			name:        "prompt or messages not found, expect err",
-			reqBodyMap:  map[string]interface{}{"model": model},
+			reqBodyMap:  map[string]any{"model": model},
 			wantErrCode: errutil.BadRequest,
 		},
 		{
 			name: "empty messages, expect err",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":    model,
-				"messages": []interface{}{},
+				"messages": []any{},
 			},
 			wantErrCode: errutil.BadRequest,
 		},
 		{
 			name: "scheduler returns error",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  model,
 				"prompt": "prompt that causes scheduler error",
 			},
@@ -332,7 +336,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 		{
 			name: "scheduler returns nil result and nil error",
-			reqBodyMap: map[string]interface{}{
+			reqBodyMap: map[string]any{
 				"model":  model,
 				"prompt": "prompt for nil,nil scheduler return",
 			},
@@ -355,7 +359,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			reqCtx := &handlers.RequestContext{
 				Request: &handlers.Request{
 					// Create a copy of the map for each test run to avoid mutation issues.
-					Body: make(map[string]interface{}),
+					Body: make(map[string]any),
 					Headers: map[string]string{
 						requtil.RequestIdHeaderKey: "test-req-id-" + test.name, // Ensure a default request ID
 					},
@@ -396,6 +400,138 @@ func TestDirector_HandleRequest(t *testing.T) {
 	}
 }
 
+// TestGetCandidatePodsForScheduling is testing getCandidatePodsForScheduling and more specifically the functionality of SubsetFilter.
+func TestGetCandidatePodsForScheduling(t *testing.T) {
+	var makeFilterMetadata = func(data []any) map[string]any {
+		return map[string]any{
+			"envoy.lb.subset_hint": map[string]any{
+				"x-gateway-destination-endpoint-subset": data,
+			},
+		}
+	}
+
+	testInput := []*corev1.Pod{
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "pod1",
+			},
+			Status: corev1.PodStatus{
+				PodIP: "10.0.0.1",
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "pod2",
+			},
+			Status: corev1.PodStatus{
+				PodIP: "10.0.0.2",
+			},
+		},
+	}
+
+	outputPod1 := &backend.Pod{
+		NamespacedName: types.NamespacedName{Name: "pod1"},
+		Address:        "10.0.0.1",
+		Labels:         map[string]string{},
+	}
+
+	outputPod2 := &backend.Pod{
+		NamespacedName: types.NamespacedName{Name: "pod2"},
+		Address:        "10.0.0.2",
+		Labels:         map[string]string{},
+	}
+
+	tests := []struct {
+		name     string
+		metadata map[string]any
+		output   []schedulingtypes.Pod
+	}{
+		{
+			name:     "SubsetFilter, filter not present — return all pods",
+			metadata: map[string]any{},
+			output: []schedulingtypes.Pod{
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod1,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod2,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+			},
+		},
+		{
+			name:     "SubsetFilter, namespace present filter not present — return all pods",
+			metadata: map[string]any{"envoy.lb.subset_hint": map[string]any{}},
+			output: []schedulingtypes.Pod{
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod1,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod2,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+			},
+		},
+		{
+			name:     "SubsetFilter, filter present with empty list — return error",
+			metadata: makeFilterMetadata([]any{}),
+			output:   []schedulingtypes.Pod{},
+		},
+		{
+			name:     "SubsetFilter, subset with one matching pod",
+			metadata: makeFilterMetadata([]any{"10.0.0.1"}),
+			output: []schedulingtypes.Pod{
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod1,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+			},
+		},
+		{
+			name:     "SubsetFilter, subset with multiple matching pods",
+			metadata: makeFilterMetadata([]any{"10.0.0.1", "10.0.0.2", "10.0.0.3"}),
+			output: []schedulingtypes.Pod{
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod1,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+				&schedulingtypes.PodMetrics{
+					Pod:          outputPod2,
+					MetricsState: backendmetrics.NewMetricsState(),
+				},
+			},
+		},
+		{
+			name:     "SubsetFilter, subset with no matching pods",
+			metadata: makeFilterMetadata([]any{"10.0.0.3"}),
+			output:   []schedulingtypes.Pod{},
+		},
+	}
+
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
+	ds := datastore.NewDatastore(t.Context(), pmf)
+	for _, testPod := range testInput {
+		ds.PodUpdateOrAddIfNotExist(testPod)
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			director := NewDirectorWithConfig(ds, &mockScheduler{}, &mockSaturationDetector{}, NewConfig())
+
+			got := director.getCandidatePodsForScheduling(context.Background(), test.metadata)
+
+			diff := cmp.Diff(test.output, got, cmpopts.SortSlices(func(a, b schedulingtypes.Pod) bool {
+				return a.GetPod().NamespacedName.String() < b.GetPod().NamespacedName.String()
+			}))
+			if diff != "" {
+				t.Errorf("Unexpected output (-want +got): %v", diff)
+			}
+		})
+	}
+}
+
 func TestRandomWeightedDraw(t *testing.T) {
 	logger := logutil.NewTestLogger()
 	// Note: These tests verify deterministic outcomes for a fixed seed (420).
@@ -520,9 +656,7 @@ func pointer(v int32) *int32 {
 }
 
 func TestDirector_HandleResponse(t *testing.T) {
-	pr1 := &testPostResponse{
-		TypeRes: "pr1",
-	}
+	pr1 := newTestPostResponse("pr1")
 
 	ctx := logutil.NewTestLoggerIntoContext(context.Background())
 	ds := datastore.NewDatastore(t.Context(), nil)
@@ -558,14 +692,25 @@ func TestDirector_HandleResponse(t *testing.T) {
 	}
 }
 
+const (
+	testPostResponseType = "test-post-response"
+)
+
 type testPostResponse struct {
-	TypeRes                 string
+	tn                      plugins.TypedName
 	lastRespOnResponse      *Response
 	lastTargetPodOnResponse string
 }
 
-func (p *testPostResponse) Type() string { return p.TypeRes }
-func (p *testPostResponse) Name() string { return "test-post-response" }
+func newTestPostResponse(name string) *testPostResponse {
+	return &testPostResponse{
+		tn: plugins.TypedName{Type: testPostResponseType, Name: name},
+	}
+}
+
+func (p *testPostResponse) TypedName() plugins.TypedName {
+	return p.tn
+}
 
 func (p *testPostResponse) PostResponse(_ context.Context, _ *schedulingtypes.LLMRequest, response *Response, targetPod *backend.Pod) {
 	p.lastRespOnResponse = response
diff --git a/pkg/epp/scheduling/framework/plugins/filter/filter_test.go b/pkg/epp/scheduling/framework/plugins/filter/filter_test.go
index 75cd790..0643fe2 100644
--- a/pkg/epp/scheduling/framework/plugins/filter/filter_test.go
+++ b/pkg/epp/scheduling/framework/plugins/filter/filter_test.go
@@ -25,8 +25,10 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/google/uuid"
 	k8stypes "k8s.io/apimachinery/pkg/types"
+
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	"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/framework/plugins/scorer"
@@ -37,14 +39,18 @@ import (
 // compile-time type assertion
 var _ framework.Filter = &filterAll{}
 
-type filterAll struct{}
+type filterAll struct {
+	tn plugins.TypedName
+}
 
-func (f *filterAll) Type() string {
-	return "filter-all"
+func (f *filterAll) TypedName() plugins.TypedName {
+	return f.tn
 }
 
-func (f *filterAll) Name() string {
-	return "test-all"
+func newFilterAll() *filterAll {
+	return &filterAll{
+		tn: plugins.TypedName{Type: "filter-all", Name: "test-all"},
+	}
 }
 
 func (f *filterAll) Filter(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, pods []types.Pod) []types.Pod {
@@ -61,7 +67,7 @@ func TestFilter(t *testing.T) {
 	}{
 		{
 			name:   "simple filter filters all pods",
-			filter: &filterAll{},
+			filter: newFilterAll(),
 			output: []types.Pod{},
 		},
 		{
@@ -256,145 +262,6 @@ func TestLoRASoftAffinityDistribution(t *testing.T) {
 	}
 }
 
-func TestSubsettingFilter(t *testing.T) {
-	var makeFilterMetadata = func(data []interface{}) map[string]any {
-		return map[string]any{
-			"envoy.lb.subset_hint": map[string]any{
-				"x-gateway-destination-endpoint-subset": data,
-			},
-		}
-	}
-
-	tests := []struct {
-		name     string
-		metadata map[string]any
-		filter   framework.Filter
-		input    []types.Pod
-		output   []types.Pod
-	}{
-		{
-			name:     "SubsetFilter, filter not present — return all pods",
-			filter:   &SubsetFilter{},
-			metadata: map[string]any{},
-			input: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-			output: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-		},
-		{
-			name:     "SubsetFilter, namespace present filter not present — return all pods",
-			filter:   &SubsetFilter{},
-			metadata: map[string]any{"envoy.lb.subset_hint": map[string]any{}},
-			input: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-			output: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-		},
-		{
-			name:     "SubsetFilter, filter present with empty list — return no pods",
-			filter:   &SubsetFilter{},
-			metadata: makeFilterMetadata([]interface{}{}),
-			input: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-			output: []types.Pod{},
-		},
-		{
-			name:     "SubsetFilter, subset with one matching pod",
-			metadata: makeFilterMetadata([]interface{}{"10.0.0.1"}),
-			filter:   &SubsetFilter{},
-			input: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-			output: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-			},
-		},
-		{
-			name:     "SubsetFilter, subset with multiple matching pods",
-			metadata: makeFilterMetadata([]interface{}{"10.0.0.1", "10.0.0.2", "10.0.0.3"}),
-			filter:   &SubsetFilter{},
-			input: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-			output: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-		},
-		{
-			name:     "SubsetFilter, subset with no matching pods",
-			metadata: makeFilterMetadata([]interface{}{"10.0.0.3"}),
-			filter:   &SubsetFilter{},
-			input: []types.Pod{
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.1"},
-				},
-				&types.PodMetrics{
-					Pod: &backend.Pod{Address: "10.0.0.2"},
-				},
-			},
-			output: []types.Pod{},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			req := types.NewLLMRequest(uuid.NewString(), "", "", nil, test.metadata)
-			got := test.filter.Filter(context.Background(), types.NewCycleState(), req, test.input)
-
-			if diff := cmp.Diff(test.output, got); diff != "" {
-				t.Errorf("Unexpected output (-want +got): %v", diff)
-			}
-		})
-	}
-}
-
 // TestDecisionTreeFilterFactory tests that the DecisionTreeFilterFactory function
 // properly instantiates DecisionTreeFilter instances
 func TestDecisionTreeFilterFactory(t *testing.T) {
@@ -408,12 +275,12 @@ func TestDecisionTreeFilterFactory(t *testing.T) {
 
 	testHandle := utils.NewTestHandle(context.Background())
 
-	testHandle.Plugins().AddPlugin("leastKvCache", leastKvCacheFilter)
-	testHandle.Plugins().AddPlugin("leastQueue", leastQueueFilter)
-	testHandle.Plugins().AddPlugin("loraAffinity", loraAffinityFilter)
-	testHandle.Plugins().AddPlugin("lowQueue", lowQueueFilter)
+	testHandle.AddPlugin("leastKvCache", leastKvCacheFilter)
+	testHandle.AddPlugin("leastQueue", leastQueueFilter)
+	testHandle.AddPlugin("loraAffinity", loraAffinityFilter)
+	testHandle.AddPlugin("lowQueue", lowQueueFilter)
 
-	testHandle.Plugins().AddPlugin("kvCacheScorer", kvCacheScorer)
+	testHandle.AddPlugin("kvCacheScorer", kvCacheScorer)
 
 	tests := []struct {
 		name       string
@@ -498,7 +365,7 @@ func TestDecisionTreeFilterFactory(t *testing.T) {
 	}
 
 	cmpOptions := cmpopts.IgnoreUnexported(LeastKVCacheFilter{}, LeastQueueFilter{},
-		LoraAffinityFilter{}, LowQueueFilter{}, scorer.KVCacheScorer{})
+		LoraAffinityFilter{}, LowQueueFilter{}, scorer.KVCacheScorer{}, plugins.TypedName{})
 
 	for _, test := range tests {
 		rawParameters := struct {
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
index ea7fab7..27e13d6 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
@@ -61,7 +61,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
 	// Simulate pod1 was picked.
-	plugin.PostCycle(context.Background(), cycleState1, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState1, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 
 	// Second request doesn't share any prefix with first one. It should be added to the cache but
 	// the pod score should be 0.
@@ -82,7 +82,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
 	// Simulate pod2 was picked.
-	plugin.PostCycle(context.Background(), cycleState2, &types.ProfileRunResult{TargetPod: pod2})
+	plugin.PostCycle(context.Background(), cycleState2, &types.ProfileRunResult{TargetPods: []types.Pod{pod2}})
 
 	// Third request shares partial prefix with first one.
 	req3 := &types.LLMRequest{
@@ -101,7 +101,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(2)/float64(3), scores[pod1], "score should be 2/3 - the model and the first prefix block match")
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
-	plugin.PostCycle(context.Background(), cycleState3, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState3, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 
 	// 4th request is same as req3 except the model is different, still no match.
 	req4 := &types.LLMRequest{
@@ -120,7 +120,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, float64(0), scores[pod1], "score for pod1")
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
-	plugin.PostCycle(context.Background(), cycleState4, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState4, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 
 	// 5th request shares partial prefix with 3rd one.
 	req5 := &types.LLMRequest{
@@ -139,7 +139,7 @@ func TestPrefixPlugin(t *testing.T) {
 	assert.Equal(t, 0.75, scores[pod1], "score should be 0.75 - the model and the first 2 prefix blocks match")
 	assert.Equal(t, float64(0), scores[pod2], "score for pod2")
 
-	plugin.PostCycle(context.Background(), cycleState5, &types.ProfileRunResult{TargetPod: pod1})
+	plugin.PostCycle(context.Background(), cycleState5, &types.ProfileRunResult{TargetPods: []types.Pod{pod1}})
 }
 
 // TestPrefixPluginStress is a stress test for the prefix scoring plugin, using prompts of increasing length.
@@ -180,7 +180,7 @@ func BenchmarkPrefixPluginStress(b *testing.B) {
 		// First cycle: simulate scheduling and insert prefix info into the cache
 		cycleState := types.NewCycleState()
 		plugin.Score(context.Background(), cycleState, req, pods)
-		plugin.PostCycle(context.Background(), cycleState, &types.ProfileRunResult{TargetPod: pod})
+		plugin.PostCycle(context.Background(), cycleState, &types.ProfileRunResult{TargetPods: []types.Pod{pod}})
 
 		// Second cycle: validate internal state
 		state, err := types.ReadCycleStateKey[*SchedulingContextState](cycleState, PrefixCachePluginType)
diff --git a/pkg/epp/scheduling/framework/plugins/picker/picker_test.go b/pkg/epp/scheduling/framework/plugins/picker/picker_test.go
new file mode 100644
index 0000000..2089ed3
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/picker/picker_test.go
@@ -0,0 +1,120 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package picker
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	k8stypes "k8s.io/apimachinery/pkg/types"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+)
+
+func TestPickMaxScorePicker(t *testing.T) {
+	pod1 := &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}}
+	pod2 := &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}}
+	pod3 := &types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}}
+
+	tests := []struct {
+		name   string
+		picker framework.Picker
+		input  []*types.ScoredPod
+		output []types.Pod
+	}{
+		{
+			name:   "Single max score",
+			picker: NewMaxScorePicker(1),
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 10},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 15},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod2, Score: 25},
+			},
+		},
+		{
+			name:   "Multiple max scores, all are equally scored",
+			picker: NewMaxScorePicker(2),
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 50},
+				{Pod: pod2, Score: 50},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod1, Score: 50},
+				&types.ScoredPod{Pod: pod2, Score: 50},
+			},
+		},
+		{
+			name:   "Multiple results sorted by highest score, more pods than needed",
+			picker: NewMaxScorePicker(2),
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 20},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod3, Score: 30},
+				&types.ScoredPod{Pod: pod2, Score: 25},
+			},
+		},
+		{
+			name:   "Multiple results sorted by highest score, less pods than needed",
+			picker: NewMaxScorePicker(4), // picker is required to return 4 pods at most, but we have only 3.
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 20},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod3, Score: 30},
+				&types.ScoredPod{Pod: pod2, Score: 25},
+				&types.ScoredPod{Pod: pod1, Score: 20},
+			},
+		},
+		{
+			name:   "Multiple results sorted by highest score, num of pods exactly needed",
+			picker: NewMaxScorePicker(3), // picker is required to return 3 pods at most, we have only 3.
+			input: []*types.ScoredPod{
+				{Pod: pod1, Score: 30},
+				{Pod: pod2, Score: 25},
+				{Pod: pod3, Score: 30},
+			},
+			output: []types.Pod{
+				&types.ScoredPod{Pod: pod1, Score: 30},
+				&types.ScoredPod{Pod: pod3, Score: 30},
+				&types.ScoredPod{Pod: pod2, Score: 25},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := test.picker.Pick(context.Background(), types.NewCycleState(), test.input)
+			got := result.TargetPods
+
+			if diff := cmp.Diff(test.output, got); diff != "" {
+				t.Errorf("Unexpected output (-want +got): %v", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/epp/scheduling/framework/scheduler_profile_test.go b/pkg/epp/scheduling/framework/scheduler_profile_test.go
index d94bb26..020223c 100644
--- a/pkg/epp/scheduling/framework/scheduler_profile_test.go
+++ b/pkg/epp/scheduling/framework/scheduler_profile_test.go
@@ -23,8 +23,9 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/uuid"
 	k8stypes "k8s.io/apimachinery/pkg/types"
+
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
-	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics" // Import config for thresholds
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
@@ -51,7 +52,7 @@ func TestSchedulePlugins(t *testing.T) {
 	tests := []struct {
 		name           string
 		profile        *SchedulerProfile
-		input          []backendmetrics.PodMetrics
+		input          []types.Pod
 		wantTargetPod  k8stypes.NamespacedName
 		targetPodScore float64
 		// Number of expected pods to score (after filter)
@@ -65,10 +66,10 @@ func TestSchedulePlugins(t *testing.T) {
 				WithScorers(NewWeightedScorer(tp1, 1), NewWeightedScorer(tp2, 1)).
 				WithPicker(pickerPlugin).
 				WithPostCyclePlugins(tp1, tp2),
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			input: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			wantTargetPod:  k8stypes.NamespacedName{Name: "pod1"},
 			targetPodScore: 1.1,
@@ -82,10 +83,10 @@ func TestSchedulePlugins(t *testing.T) {
 				WithScorers(NewWeightedScorer(tp1, 60), NewWeightedScorer(tp2, 40)).
 				WithPicker(pickerPlugin).
 				WithPostCyclePlugins(tp1, tp2),
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			input: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			wantTargetPod:  k8stypes.NamespacedName{Name: "pod1"},
 			targetPodScore: 50,
@@ -99,10 +100,10 @@ func TestSchedulePlugins(t *testing.T) {
 				WithScorers(NewWeightedScorer(tp1, 1), NewWeightedScorer(tp2, 1)).
 				WithPicker(pickerPlugin).
 				WithPostCyclePlugins(tp1, tp2),
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				&backendmetrics.FakePodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			input: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				&types.PodMetrics{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			numPodsToScore: 0,
 			err:            true, // no available pods to server after filter all
@@ -129,7 +130,7 @@ func TestSchedulePlugins(t *testing.T) {
 				RequestId:   uuid.NewString(),
 			}
 			// Run profile cycle
-			got, err := test.profile.Run(context.Background(), request, types.NewCycleState(), types.ToSchedulerPodMetrics(test.input))
+			got, err := test.profile.Run(context.Background(), request, types.NewCycleState(), test.input)
 
 			// Validate error state
 			if test.err != (err != nil) {
@@ -141,11 +142,12 @@ func TestSchedulePlugins(t *testing.T) {
 			}
 
 			// Validate output
-			wantPod := &types.PodMetrics{
-				Pod: &backend.Pod{NamespacedName: test.wantTargetPod, Labels: make(map[string]string)},
-			}
 			wantRes := &types.ProfileRunResult{
-				TargetPod: wantPod,
+				TargetPods: []types.Pod{
+					&types.PodMetrics{
+						Pod: &backend.Pod{NamespacedName: test.wantTargetPod},
+					},
+				},
 			}
 
 			if diff := cmp.Diff(wantRes, got); diff != "" {
@@ -155,32 +157,32 @@ func TestSchedulePlugins(t *testing.T) {
 			for _, plugin := range test.profile.filters {
 				tp, _ := plugin.(*testPlugin)
 				if tp.FilterCallCount != 1 {
-					t.Errorf("Plugin %s Filter() called %d times, expected 1", plugin.Type(), tp.FilterCallCount)
+					t.Errorf("Plugin '%s' Filter() called %d times, expected 1", plugin.TypedName(), tp.FilterCallCount)
 				}
 			}
 			for _, plugin := range test.profile.scorers {
 				tp, _ := plugin.Scorer.(*testPlugin)
 				if tp.ScoreCallCount != 1 {
-					t.Errorf("Plugin %s Score() called %d times, expected 1", plugin.Type(), tp.ScoreCallCount)
+					t.Errorf("Plugin '%s' Score() called %d times, expected 1", plugin.TypedName(), tp.ScoreCallCount)
 				}
 				if test.numPodsToScore != tp.NumOfScoredPods {
-					t.Errorf("Plugin %s Score() called with %d pods, expected %d", plugin.Type(), tp.NumOfScoredPods, test.numPodsToScore)
+					t.Errorf("Plugin '%s' Score() called with %d pods, expected %d", plugin.TypedName(), tp.NumOfScoredPods, test.numPodsToScore)
 				}
 			}
 			tp, _ := test.profile.picker.(*testPlugin)
 			if tp.NumOfPickerCandidates != test.numPodsToScore {
-				t.Errorf("Picker plugin %s Pick() called with %d candidates, expected %d", tp.Type(), tp.NumOfPickerCandidates, tp.NumOfScoredPods)
+				t.Errorf("Picker plugin '%s' Pick() called with %d candidates, expected %d", tp.TypedName(), tp.NumOfPickerCandidates, tp.NumOfScoredPods)
 			}
 			if tp.PickCallCount != 1 {
-				t.Errorf("Picker plugin %s Pick() called %d times, expected 1", tp.Type(), tp.PickCallCount)
+				t.Errorf("Picker plugin '%s' Pick() called %d times, expected 1", tp.TypedName(), tp.PickCallCount)
 			}
-			if tp.WinnderPodScore != test.targetPodScore {
-				t.Errorf("winnder pod score %v, expected %v", tp.WinnderPodScore, test.targetPodScore)
+			if tp.WinnerPodScore != test.targetPodScore {
+				t.Errorf("winner pod score %v, expected %v", tp.WinnerPodScore, test.targetPodScore)
 			}
 			for _, plugin := range test.profile.postCyclePlugins {
 				tp, _ := plugin.(*testPlugin)
-				if tp.PostScheduleCallCount != 1 {
-					t.Errorf("Plugin %s PostSchedule() called %d times, expected 1", plugin.Type(), tp.PostScheduleCallCount)
+				if tp.PostCycleCallCount != 1 {
+					t.Errorf("Plugin '%s' PostCycle() called %d times, expected 1", plugin.TypedName(), tp.PostCycleCallCount)
 				}
 			}
 		})
@@ -195,21 +197,23 @@ var _ PostCycle = &testPlugin{}
 
 // testPlugin is an implementation useful in unit tests.
 type testPlugin struct {
+	typedName             plugins.TypedName
 	TypeRes               string
 	ScoreCallCount        int
 	NumOfScoredPods       int
 	ScoreRes              float64
 	FilterCallCount       int
 	FilterRes             []k8stypes.NamespacedName
-	PostScheduleCallCount int
+	PostCycleCallCount    int
 	PickCallCount         int
 	NumOfPickerCandidates int
 	PickRes               k8stypes.NamespacedName
-	WinnderPodScore       float64
+	WinnerPodScore        float64
 }
 
-func (tp *testPlugin) Type() string { return tp.TypeRes }
-func (tp *testPlugin) Name() string { return "test-plugin" }
+func (tp *testPlugin) TypedName() plugins.TypedName {
+	return tp.typedName
+}
 
 func (tp *testPlugin) Filter(_ context.Context, _ *types.CycleState, _ *types.LLMRequest, pods []types.Pod) []types.Pod {
 	tp.FilterCallCount++
@@ -231,26 +235,26 @@ func (tp *testPlugin) Pick(_ context.Context, _ *types.CycleState, scoredPods []
 	tp.PickCallCount++
 	tp.NumOfPickerCandidates = len(scoredPods)
 
-	var winnerPod types.Pod
+	winnerPods := []types.Pod{}
 	for _, scoredPod := range scoredPods {
 		if scoredPod.GetPod().NamespacedName.String() == tp.PickRes.String() {
-			winnerPod = scoredPod.Pod
-			tp.WinnderPodScore = scoredPod.Score
+			winnerPods = append(winnerPods, scoredPod.Pod)
+			tp.WinnerPodScore = scoredPod.Score
 		}
 	}
 
-	return &types.ProfileRunResult{TargetPod: winnerPod}
+	return &types.ProfileRunResult{TargetPods: winnerPods}
 }
 
 func (tp *testPlugin) PostCycle(_ context.Context, _ *types.CycleState, res *types.ProfileRunResult) {
-	tp.PostScheduleCallCount++
+	tp.PostCycleCallCount++
 }
 
 func (tp *testPlugin) reset() {
 	tp.FilterCallCount = 0
 	tp.ScoreCallCount = 0
 	tp.NumOfScoredPods = 0
-	tp.PostScheduleCallCount = 0
+	tp.PostCycleCallCount = 0
 	tp.PickCallCount = 0
 	tp.NumOfPickerCandidates = 0
 }
diff --git a/pkg/epp/scheduling/scheduler_test.go b/pkg/epp/scheduling/scheduler_test.go
index 720a6b4..996d152 100644
--- a/pkg/epp/scheduling/scheduler_test.go
+++ b/pkg/epp/scheduling/scheduler_test.go
@@ -33,32 +33,32 @@ func TestSchedule(t *testing.T) {
 	tests := []struct {
 		name    string
 		req     *types.LLMRequest
-		input   []backendmetrics.PodMetrics
+		input   []types.Pod
 		wantRes *types.SchedulingResult
 		err     bool
 	}{
 		{
 			name: "no candidate pods",
 			req: &types.LLMRequest{
-				TargetModel: "any-model",
 				RequestId:   uuid.NewString(),
+				TargetModel: "any-model",
 			},
-			input:   []backendmetrics.PodMetrics{},
+			input:   []types.Pod{},
 			wantRes: nil,
 			err:     true,
 		},
 		{
 			name: "finds optimal pod",
 			req: &types.LLMRequest{
-				TargetModel: "critical",
 				RequestId:   uuid.NewString(),
+				TargetModel: "critical",
 			},
 			// pod2 will be picked because it has relatively low queue size, with the requested
 			// model being active, and has low KV cache.
-			input: []backendmetrics.PodMetrics{
-				&backendmetrics.FakePodMetrics{
+			input: []types.Pod{
+				&types.PodMetrics{
 					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
-					Metrics: &backendmetrics.MetricsState{
+					MetricsState: &backendmetrics.MetricsState{
 						WaitingQueueSize:    0,
 						KVCacheUsagePercent: 0.2,
 						MaxActiveModels:     2,
@@ -68,9 +68,9 @@ func TestSchedule(t *testing.T) {
 						},
 					},
 				},
-				&backendmetrics.FakePodMetrics{
+				&types.PodMetrics{
 					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
-					Metrics: &backendmetrics.MetricsState{
+					MetricsState: &backendmetrics.MetricsState{
 						WaitingQueueSize:    3,
 						KVCacheUsagePercent: 0.1,
 						MaxActiveModels:     2,
@@ -80,9 +80,9 @@ func TestSchedule(t *testing.T) {
 						},
 					},
 				},
-				&backendmetrics.FakePodMetrics{
+				&types.PodMetrics{
 					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
-					Metrics: &backendmetrics.MetricsState{
+					MetricsState: &backendmetrics.MetricsState{
 						WaitingQueueSize:    10,
 						KVCacheUsagePercent: 0.2,
 						MaxActiveModels:     2,
@@ -95,18 +95,19 @@ func TestSchedule(t *testing.T) {
 			wantRes: &types.SchedulingResult{
 				ProfileResults: map[string]*types.ProfileRunResult{
 					"default": {
-						TargetPod: &types.ScoredPod{
-							Pod: &types.PodMetrics{
-								Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}, Labels: make(map[string]string)},
-								MetricsState: &backendmetrics.MetricsState{
-									WaitingQueueSize:    3,
-									KVCacheUsagePercent: 0.1,
-									MaxActiveModels:     2,
-									ActiveModels: map[string]int{
-										"foo":      1,
-										"critical": 1,
+						TargetPods: []types.Pod{
+							&types.ScoredPod{
+								Pod: &types.PodMetrics{
+									Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
+									MetricsState: &backendmetrics.MetricsState{
+										WaitingQueueSize:    3,
+										KVCacheUsagePercent: 0.1,
+										MaxActiveModels:     2,
+										ActiveModels: map[string]int{
+											"foo":      1,
+											"critical": 1,
+										},
 									},
-									WaitingModels: map[string]int{},
 								},
 							},
 						},
@@ -120,7 +121,7 @@ func TestSchedule(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			scheduler := NewScheduler()
-			got, err := scheduler.Schedule(context.Background(), test.req, types.ToSchedulerPodMetrics(test.input))
+			got, err := scheduler.Schedule(context.Background(), test.req, test.input)
 			if test.err != (err != nil) {
 				t.Errorf("Unexpected error, got %v, want %v", err, test.err)
 			}
diff --git a/pkg/epp/util/error/error_test.go b/pkg/epp/util/error/error_test.go
index 7393c44..8905e84 100644
--- a/pkg/epp/util/error/error_test.go
+++ b/pkg/epp/util/error/error_test.go
@@ -43,6 +43,14 @@ func TestError_Error(t *testing.T) {
 			},
 			want: "inference gateway: Internal - unexpected condition",
 		},
+		{
+			name: "ServiceUnavailable error",
+			err: Error{
+				Code: ServiceUnavailable,
+				Msg:  "service unavailable",
+			},
+			want: "inference gateway: ServiceUnavailable - service unavailable",
+		},
 		{
 			name: "ModelServerError",
 			err: Error{
@@ -124,6 +132,14 @@ func TestCanonicalCode(t *testing.T) {
 			},
 			want: Internal,
 		},
+		{
+			name: "Error type with ServiceUnavailable code",
+			err: Error{
+				Code: ServiceUnavailable,
+				Msg:  "Service unavailable error",
+			},
+			want: ServiceUnavailable,
+		},
 		{
 			name: "Error type with ModelServerError code",
 			err: Error{
@@ -205,6 +221,7 @@ func TestErrorConstants(t *testing.T) {
 		Unknown:                        "Unknown",
 		BadRequest:                     "BadRequest",
 		Internal:                       "Internal",
+		ServiceUnavailable:             "ServiceUnavailable",
 		ModelServerError:               "ModelServerError",
 		BadConfiguration:               "BadConfiguration",
 		InferencePoolResourceExhausted: "InferencePoolResourceExhausted",
diff --git a/pkg/epp/util/request/body_test.go b/pkg/epp/util/request/body_test.go
index 696bfd5..ce5a939 100644
--- a/pkg/epp/util/request/body_test.go
+++ b/pkg/epp/util/request/body_test.go
@@ -23,23 +23,23 @@ import (
 func TestExtractPromptFromRequestBody(t *testing.T) {
 	tests := []struct {
 		name    string
-		body    map[string]interface{}
+		body    map[string]any
 		want    string
 		wantErr bool
 		errType error
 	}{
 		{
 			name: "chat completions request body",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"model": "test",
-				"messages": []interface{}{
-					map[string]interface{}{
+				"messages": []any{
+					map[string]any{
 						"role": "system", "content": "this is a system message",
 					},
-					map[string]interface{}{
+					map[string]any{
 						"role": "user", "content": "hello",
 					},
-					map[string]interface{}{
+					map[string]any{
 						"role": "assistant", "content": "hi, what can I do for you?",
 					},
 				},
@@ -50,7 +50,7 @@ func TestExtractPromptFromRequestBody(t *testing.T) {
 		},
 		{
 			name: "completions request body",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"model":  "test",
 				"prompt": "test prompt",
 			},
@@ -58,16 +58,16 @@ func TestExtractPromptFromRequestBody(t *testing.T) {
 		},
 		{
 			name: "invalid prompt format",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"model": "test",
-				"prompt": []interface{}{
-					map[string]interface{}{
+				"prompt": []any{
+					map[string]any{
 						"role": "system", "content": "this is a system message",
 					},
-					map[string]interface{}{
+					map[string]any{
 						"role": "user", "content": "hello",
 					},
-					map[string]interface{}{
+					map[string]any{
 						"role": "assistant", "content": "hi, what can I",
 					},
 				},
@@ -76,9 +76,9 @@ func TestExtractPromptFromRequestBody(t *testing.T) {
 		},
 		{
 			name: "invalid messaged format",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"model": "test",
-				"messages": map[string]interface{}{
+				"messages": map[string]any{
 					"role": "system", "content": "this is a system message",
 				},
 			},
@@ -86,7 +86,7 @@ func TestExtractPromptFromRequestBody(t *testing.T) {
 		},
 		{
 			name: "prompt does not exist",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"model": "test",
 			},
 			wantErr: true,
@@ -110,25 +110,25 @@ func TestExtractPromptFromRequestBody(t *testing.T) {
 func TestExtractPromptField(t *testing.T) {
 	tests := []struct {
 		name    string
-		body    map[string]interface{}
+		body    map[string]any
 		want    string
 		wantErr bool
 	}{
 		{
 			name: "valid prompt",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"prompt": "test prompt",
 			},
 			want: "test prompt",
 		},
 		{
 			name:    "prompt not found",
-			body:    map[string]interface{}{},
+			body:    map[string]any{},
 			wantErr: true,
 		},
 		{
 			name: "non-string prompt",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"prompt": 123,
 			},
 			wantErr: true,
@@ -152,23 +152,23 @@ func TestExtractPromptField(t *testing.T) {
 func TestExtractPromptFromMessagesField(t *testing.T) {
 	tests := []struct {
 		name    string
-		body    map[string]interface{}
+		body    map[string]any
 		want    string
 		wantErr bool
 	}{
 		{
 			name: "valid messages",
-			body: map[string]interface{}{
-				"messages": []interface{}{
-					map[string]interface{}{"role": "user", "content": "test1"},
-					map[string]interface{}{"role": "assistant", "content": "test2"},
+			body: map[string]any{
+				"messages": []any{
+					map[string]any{"role": "user", "content": "test1"},
+					map[string]any{"role": "assistant", "content": "test2"},
 				},
 			},
 			want: "
user\ntest1
\n
assistant\ntest2
\n",
 		},
 		{
 			name: "invalid messages format",
-			body: map[string]interface{}{
+			body: map[string]any{
 				"messages": "invalid",
 			},
 			wantErr: true,
diff --git a/pkg/epp/util/request/metadata_test.go b/pkg/epp/util/request/metadata_test.go
index 0a0c71e..f03d83e 100644
--- a/pkg/epp/util/request/metadata_test.go
+++ b/pkg/epp/util/request/metadata_test.go
@@ -27,9 +27,9 @@ import (
 
 func TestExtractMetadataValues(t *testing.T) {
 	var makeFilterMetadata = func() map[string]*structpb.Struct {
-		structVal, _ := structpb.NewStruct(map[string]interface{}{
+		structVal, _ := structpb.NewStruct(map[string]any{
 			"hello":      "world",
-			"random-key": []interface{}{"hello", "world"},
+			"random-key": []any{"hello", "world"},
 		})
 
 		return map[string]*structpb.Struct{
@@ -46,9 +46,9 @@ func TestExtractMetadataValues(t *testing.T) {
 			name:     "Exact match",
 			metadata: makeFilterMetadata(),
 			expected: map[string]any{
-				"key-1": map[string]interface{}{
+				"key-1": map[string]any{
 					"hello":      "world",
-					"random-key": []interface{}{"hello", "world"},
+					"random-key": []any{"hello", "world"},
 				},
 			},
 		},
diff --git a/test/integration/epp/hermetic_test.go b/test/integration/epp/hermetic_test.go
index 2b9bb76..6d439d1 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -821,9 +821,9 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 					Response: &extProcPb.ProcessingResponse_ImmediateResponse{
 						ImmediateResponse: &extProcPb.ImmediateResponse{
 							Status: &envoyTypePb.HttpStatus{
-								Code: envoyTypePb.StatusCode_TooManyRequests,
+								Code: envoyTypePb.StatusCode_ServiceUnavailable,
 							},
-							Body: []byte("inference gateway: InferencePoolResourceExhausted - failed to find target pod: failed to run scheduler profile 'default'"),
+							Body: []byte("inference gateway: ServiceUnavailable - failed to find candidate pods for serving the request"),
 						},
 					},
 				},
diff --git a/test/integration/util.go b/test/integration/util.go
index a1baa33..925107b 100644
--- a/test/integration/util.go
+++ b/test/integration/util.go
@@ -87,7 +87,7 @@ func StreamedRequest(t *testing.T, client extProcPb.ExternalProcessor_ProcessCli
 }
 
 func GenerateRequest(logger logr.Logger, prompt, model string, filterMetadata []string) *extProcPb.ProcessingRequest {
-	j := map[string]interface{}{
+	j := map[string]any{
 		"prompt":      prompt,
 		"max_tokens":  100,
 		"temperature": 0,
@@ -139,12 +139,12 @@ func GenerateStreamedRequestSet(logger logr.Logger, prompt, model string, filter
 
 func GenerateRequestMetadata(filterMetadata []string) map[string]*structpb.Struct {
 	metadata := make(map[string]*structpb.Struct)
-	interfaceList := make([]interface{}, len(filterMetadata))
+	interfaceList := make([]any, len(filterMetadata))
 	for i, val := range filterMetadata {
 		interfaceList[i] = val
 	}
 	if filterMetadata != nil {
-		structVal, _ := structpb.NewStruct(map[string]interface{}{
+		structVal, _ := structpb.NewStruct(map[string]any{
 			"x-gateway-destination-endpoint-subset": interfaceList,
 		})
 		metadata["envoy.lb.subset_hint"] = structVal
diff --git a/test/utils/handle.go b/test/utils/handle.go
index 417346f..4a29dda 100644
--- a/test/utils/handle.go
+++ b/test/utils/handle.go
@@ -24,8 +24,8 @@ import (
 
 // testHandle is an implmentation of plugins.Handle for test purposes
 type testHandle struct {
-	ctx     context.Context
-	plugins plugins.HandlePlugins
+	ctx context.Context
+	plugins.HandlePlugins
 }
 
 // Context returns a context the plugins can use, if they need one
@@ -33,39 +33,35 @@ func (h *testHandle) Context() context.Context {
 	return h.ctx
 }
 
-func (h *testHandle) Plugins() plugins.HandlePlugins {
-	return h.plugins
-}
-
 type testHandlePlugins struct {
-	thePlugins map[string]plugins.Plugin
+	plugins map[string]plugins.Plugin
 }
 
 func (h *testHandlePlugins) Plugin(name string) plugins.Plugin {
-	return h.thePlugins[name]
+	return h.plugins[name]
 }
 
 func (h *testHandlePlugins) AddPlugin(name string, plugin plugins.Plugin) {
-	h.thePlugins[name] = plugin
+	h.plugins[name] = plugin
 }
 
 func (h *testHandlePlugins) GetAllPlugins() []plugins.Plugin {
 	result := make([]plugins.Plugin, 0)
-	for _, plugin := range h.thePlugins {
+	for _, plugin := range h.plugins {
 		result = append(result, plugin)
 	}
 	return result
 }
 
 func (h *testHandlePlugins) GetAllPluginsWithNames() map[string]plugins.Plugin {
-	return h.thePlugins
+	return h.plugins
 }
 
 func NewTestHandle(ctx context.Context) plugins.Handle {
 	return &testHandle{
 		ctx: ctx,
-		plugins: &testHandlePlugins{
-			thePlugins: map[string]plugins.Plugin{},
+		HandlePlugins: &testHandlePlugins{
+			plugins: map[string]plugins.Plugin{},
 		},
 	}
 }