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

Issue:
CI: Add Tooling to Check for Incompatible CRD Changes
Add tooling, e.g. [crdify](https://github.com/kubernetes-sigs/crdify), to CI that checks for incompatible CRD changes.

Hints from issue comments:
@bexxmodd: I've never done any CI setup on Github so this is good opportunity for me try it out.

/assign

Solution patch:
diff --git a/.github/workflows/crd-validation.yml b/.github/workflows/crd-validation.yml
new file mode 100644
index 0000000..f43e4b7
--- /dev/null
+++ b/.github/workflows/crd-validation.yml
@@ -0,0 +1,51 @@
+name: CRD Validation
+
+on:
+  pull_request:
+    types: [opened, edited, synchronize, reopened]
+
+permissions:
+  contents: read
+
+jobs:
+  crd-validation:
+    name: CRD Validation Check
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+          fetch-depth: 0
+
+      - name: Set up Go
+        uses: actions/setup-go@v6
+        with:
+          go-version: "1.25"
+          cache: true
+
+      - name: Install crdify
+        run: |
+          go install sigs.k8s.io/crdify@latest
+
+      - name: Run CRD Validation Check
+        run: |
+          git fetch origin ${{ github.base_ref }}:upstream_base
+          BASE_SHA=$(git rev-parse upstream_base)
+
+          FAILED=0
+          for crd in config/crd/bases/*.yaml; do
+            if ! crdify "git://${BASE_SHA}?path=$crd" "git://HEAD?path=$crd"; then
+              echo "❌ Incompatible change detected in $crd"
+              ((FAILED++))
+            else
+              echo "✅ $crd is valid"
+            fi
+          done
+
+          if [ "$FAILED" -gt 0 ]; then
+            echo "::error::Validation failed! Found $FAILED incompatible CRD change(s)."
+            exit 1
+          fi
+
+          echo "All CRDs are compatible."
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index 4ccd573..d7854d7 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -12,3 +12,4 @@ aliases:
   - elevran
   - liu-cong
   - robscott
+  - shmuelk
diff --git a/bbr.Dockerfile b/bbr.Dockerfile
index 1c294c4..07e4b66 100644
--- a/bbr.Dockerfile
+++ b/bbr.Dockerfile
@@ -15,11 +15,11 @@ COPY go.mod go.sum ./
 RUN go mod download
 
 # Sources
-COPY cmd/bbr ./cmd
+COPY cmd/bbr ./cmd/bbr
 COPY pkg ./pkg
 COPY internal ./internal
 COPY api ./api
-WORKDIR /src/cmd
+WORKDIR /src/cmd/bbr
 RUN go build -o /bbr
 
 ## Multistage deploy
diff --git a/benchmarking/prefix-cache-aware/high-cache-values.yaml b/benchmarking/prefix-cache-aware/high-cache-values.yaml
index c385b73..da84940 100644
--- a/benchmarking/prefix-cache-aware/high-cache-values.yaml
+++ b/benchmarking/prefix-cache-aware/high-cache-values.yaml
@@ -23,6 +23,12 @@ logLevel: INFO
 # NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/dataset.json.
 gcsPath: ""
 
+# An S3 bucket path that points to the dataset file.
+# The file will be copied from this path to the local file system
+# at /dataset/s3-dataset.json for use during the run.
+# NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/s3-dataset.json.
+s3Path: ""
+
 # hfToken optionally creates a secret with the specified token.
 # Can be set using helm install --set hftoken=<token>
 hfToken: ""
diff --git a/benchmarking/prefix-cache-aware/low-cache-values.yaml b/benchmarking/prefix-cache-aware/low-cache-values.yaml
index f7b960c..ac44735 100644
--- a/benchmarking/prefix-cache-aware/low-cache-values.yaml
+++ b/benchmarking/prefix-cache-aware/low-cache-values.yaml
@@ -23,6 +23,12 @@ logLevel: INFO
 # NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/dataset.json.
 gcsPath: ""
 
+# An S3 bucket path that points to the dataset file.
+# The file will be copied from this path to the local file system
+# at /dataset/s3-dataset.json for use during the run.
+# NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/s3-dataset.json.
+s3Path: ""
+
 # hfToken optionally creates a secret with the specified token.
 # Can be set using helm install --set hftoken=<token>
 hfToken: ""
diff --git a/benchmarking/single-workload/decode-heavy-values.yaml b/benchmarking/single-workload/decode-heavy-values.yaml
new file mode 100644
index 0000000..a5811e0
--- /dev/null
+++ b/benchmarking/single-workload/decode-heavy-values.yaml
@@ -0,0 +1,76 @@
+# Decode Heavy Configuration
+job:
+  image:
+    repository: quay.io/inference-perf/inference-perf
+    tag: "0.2.0" # Defaults to .Chart.AppVersion
+  serviceAccountName: ""
+  nodeSelector: {}
+  # Example resources:
+  # resources:
+  #   requests:
+  #     cpu: "1"
+  #     memory: "4Gi"
+  #   limits:
+  #     cpu: "2"
+  #     memory: "8Gi"
+  resources: {}
+
+logLevel: INFO
+
+# A GCS bucket path that points to the dataset file.
+# The file will be copied from this path to the local file system
+# at /dataset/dataset.json for use during the run.
+# NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/dataset.json.
+gcsPath: ""
+
+# An S3 bucket path that points to the dataset file.
+# The file will be copied from this path to the local file system
+# at /dataset/s3-dataset.json for use during the run.
+# NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/s3-dataset.json.
+s3Path: ""
+
+# hfToken optionally creates a secret with the specified token.
+# Can be set using helm install --set hftoken=<token>
+hfToken: ""
+
+config:
+  load:
+    type: constant
+    interval: 15
+    stages:
+    - rate: 200
+      duration: 60
+    - rate: 210
+      duration: 60
+    - rate: 220
+      duration: 60
+    worker_max_concurrency: 1000
+  api:
+    type: completion
+    streaming: true
+  server:
+    type: vllm
+    model_name: meta-llama/Llama-3.1-8B-Instruct
+    base_url: http://0.0.0.0:8000
+    ignore_eos: true
+  tokenizer:
+    pretrained_model_name_or_path: meta-llama/Llama-3.1-8B-Instruct
+  data:
+    type: infinity_instruct
+    path: ""
+    input_distribution:
+      max: 1024
+    output_distribution:
+      max: 1024
+  metrics:
+    type: prometheus
+    prometheus:
+      google_managed: true
+  report:
+    request_lifecycle:
+      summary: true
+      per_stage: true
+      per_request: true
+    prometheus:
+      summary: true
+      per_stage: true
diff --git a/benchmarking/single-workload/prefill-heavy-values.yaml b/benchmarking/single-workload/prefill-heavy-values.yaml
new file mode 100644
index 0000000..4ab4470
--- /dev/null
+++ b/benchmarking/single-workload/prefill-heavy-values.yaml
@@ -0,0 +1,77 @@
+# Prefill Heavy Configuration
+job:
+  image:
+    repository: quay.io/inference-perf/inference-perf
+    tag: "0.2.0" # Defaults to .Chart.AppVersion
+  serviceAccountName: ""
+  nodeSelector: {}
+  # Example resources:
+  # resources:
+  #   requests:
+  #     cpu: "1"
+  #     memory: "4Gi"
+  #   limits:
+  #     cpu: "2"
+  #     memory: "8Gi"
+  resources: {}
+
+logLevel: INFO
+
+# A GCS bucket path that points to the dataset file.
+# The file will be copied from this path to the local file system
+# at /dataset/dataset.json for use during the run.
+# NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/dataset.json.
+gcsPath: ""
+
+# An S3 bucket path that points to the dataset file.
+# The file will be copied from this path to the local file system
+# at /dataset/s3-dataset.json for use during the run.
+# NOTE: For this dataset to be used, config.data.path must also be explicitly set to /dataset/s3-dataset.json.
+s3Path: ""
+
+# hfToken optionally creates a secret with the specified token.
+# Can be set using helm install --set hftoken=<token>
+hfToken: ""
+
+config:
+  load:
+    type: constant
+    interval: 15
+    stages:
+    - rate: 300
+      duration: 30
+    - rate: 310
+      duration: 30
+    - rate: 320
+      duration: 30
+    - rate: 330
+      duration: 30
+  api:
+    type: completion
+    streaming: true
+  server:
+    type: vllm
+    model_name: meta-llama/Llama-3.1-8B-Instruct
+    base_url: http://0.0.0.0:8000
+    ignore_eos: true
+  tokenizer:
+    pretrained_model_name_or_path: meta-llama/Llama-3.1-8B-Instruct
+  data:
+    type: billsum_conversations
+    path: ""
+    input_distribution:
+      max: 1024
+    output_distribution:
+      max: 1024
+  metrics:
+    type: prometheus
+    prometheus:
+      google_managed: true
+  report:
+    request_lifecycle:
+      summary: true
+      per_stage: true
+      per_request: true
+    prometheus:
+      summary: true
+      per_stage: true
diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go
index e6caafa..24a59eb 100644
--- a/cmd/epp/runner/runner.go
+++ b/cmd/epp/runner/runner.go
@@ -25,7 +25,10 @@ import (
 	"net/http"
 	"net/http/pprof"
 	"os"
+	"regexp"
 	"runtime"
+	"strconv"
+	"strings"
 	"sync/atomic"
 
 	"github.com/go-logr/logr"
@@ -34,16 +37,18 @@ import (
 	"go.uber.org/zap/zapcore"
 	"google.golang.org/grpc"
 	healthPb "google.golang.org/grpc/health/grpc_health_v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/rest"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
-
 	configapi "sigs.k8s.io/gateway-api-inference-extension/apix/config/v1alpha1"
 	"sigs.k8s.io/gateway-api-inference-extension/internal/runnable"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
@@ -60,6 +65,7 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics/collectors"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
+	testresponsereceived "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol/plugins/test/responsereceived"
 	"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/plugins/multi/prefix"
@@ -75,12 +81,15 @@ import (
 
 const (
 	// enableExperimentalDatalayerV2 defines the environment variable used as feature flag for the pluggable data layer.
+	// DEPRECATION NOTICE - this env var will be removed in the next version as we switch to configuring the EPP using FeatureGates in the config file.
 	enableExperimentalDatalayerV2 = "ENABLE_EXPERIMENTAL_DATALAYER_V2"
 	// enableExperimentalFlowControlLayer defines the environment variable used as a feature flag for the pluggable flow
 	// control layer.
+	// DEPRECATION NOTICE - this env var will be removed in the next version as we switch to configuring the EPP using FeatureGates in the config file.
 	enableExperimentalFlowControlLayer = "ENABLE_EXPERIMENTAL_FLOW_CONTROL_LAYER"
 
 	// Saturation Detector deprecated configuration environment variables
+	// DEPRECATION NOTICE - these env vars will be removed in the next version as we switch to configuring the EPP using the config file.
 	EnvSdQueueDepthThreshold       = "SD_QUEUE_DEPTH_THRESHOLD"
 	EnvSdKVCacheUtilThreshold      = "SD_KV_CACHE_UTIL_THRESHOLD"
 	EnvSdMetricsStalenessThreshold = "SD_METRICS_STALENESS_THRESHOLD"
@@ -107,6 +116,8 @@ var (
 	poolName            = flag.String("pool-name", runserver.DefaultPoolName, "Name of the InferencePool this Endpoint Picker is associated with.")
 	poolGroup           = flag.String("pool-group", runserver.DefaultPoolGroup, "group of the InferencePool this Endpoint Picker is associated with.")
 	poolNamespace       = flag.String("pool-namespace", "", "Namespace of the InferencePool this Endpoint Picker is associated with.")
+	endpointSelector    = flag.String("endpoint-selector", "", "selector to filter model server pods on, only key=value paris is supported. Format: a comma-separated list of key value paris,  e.g., 'app=vllm-llama3-8b-instruct,env=prod'.")
+	endpointTargetPorts = flag.String("endpoint-target-ports", "", "target ports of model server pods. Format: a comma-separated list of numbers, e.g., '3000,3001,3002'")
 	logVerbosity        = flag.Int("v", logging.DEFAULT, "number for the log level verbosity")
 	secureServing       = flag.Bool("secure-serving", runserver.DefaultSecureServing, "Enables secure serving. Defaults to true.")
 	healthChecking      = flag.Bool("health-checking", runserver.DefaultHealthChecking, "Enables health checking")
@@ -144,13 +155,14 @@ func NewRunner() *Runner {
 	return &Runner{
 		eppExecutableName:    "GIE",
 		requestControlConfig: requestcontrol.NewConfig(), // default requestcontrol config has empty plugin list
+		customCollectors:     []prometheus.Collector{},
 	}
 }
 
 // Runner is used to run epp with its plugins
 type Runner struct {
 	eppExecutableName    string // the EPP executable name
-	featureGates         config.FeatureConfig
+	featureGates         map[string]bool
 	requestControlConfig *requestcontrol.Config
 	schedulerConfig      *scheduling.SchedulerConfig
 	customCollectors     []prometheus.Collector
@@ -215,32 +227,38 @@ func (r *Runner) Run(ctx context.Context) error {
 		return err
 	}
 
-	rawConfig, featureGates, err := r.parseConfigurationPhaseOne(ctx)
+	rawConfig, err := r.parseConfigurationPhaseOne(ctx)
 	if err != nil {
 		setupLog.Error(err, "Failed to parse configuration")
 		return err
 	}
-	r.featureGates = featureGates
 
 	// --- Setup Datastore ---
 	epf, err := r.setupMetricsCollection(setupLog, r.featureGates[datalayer.FeatureGate])
 	if err != nil {
 		return err
 	}
-	datastore := datastore.NewDatastore(ctx, epf, int32(*modelServerMetricsPort))
 
-	eppConfig, err := r.parseConfigurationPhaseTwo(ctx, rawConfig, datastore)
+	gknn, err := extractGKNN(*poolName, *poolGroup, *poolNamespace, *endpointSelector)
+	if err != nil {
+		setupLog.Error(err, "Failed to extract GKNN")
+		return err
+	}
+	disableK8sCrdReconcile := *endpointSelector != ""
+	ds, err := setupDatastore(setupLog, ctx, epf, int32(*modelServerMetricsPort), disableK8sCrdReconcile, *poolName, *poolNamespace, *endpointSelector, *endpointTargetPorts)
+	if err != nil {
+		setupLog.Error(err, "Failed to setup datastore")
+		return err
+	}
+	eppConfig, err := r.parseConfigurationPhaseTwo(ctx, rawConfig, ds)
 	if err != nil {
 		setupLog.Error(err, "Failed to parse configuration")
 		return err
 	}
 
 	// --- Setup Metrics Server ---
-	customCollectors := []prometheus.Collector{collectors.NewInferencePoolMetricsCollector(datastore)}
-	if r.customCollectors != nil {
-		customCollectors = append(customCollectors, r.customCollectors...)
-	}
-	metrics.Register(customCollectors...)
+	r.customCollectors = append(r.customCollectors, collectors.NewInferencePoolMetricsCollector(ds))
+	metrics.Register(r.customCollectors...)
 	metrics.RecordInferenceExtensionInfo(version.CommitSHA, version.BuildRef)
 	// Register metrics handler.
 	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
@@ -258,34 +276,10 @@ func (r *Runner) Run(ctx context.Context) error {
 		}(),
 	}
 
-	// Determine pool namespace: if --pool-namespace is non-empty, use it; else NAMESPACE env var; else default
-	resolvePoolNamespace := func() string {
-		if *poolNamespace != "" {
-			return *poolNamespace
-		}
-		if nsEnv := os.Getenv("NAMESPACE"); nsEnv != "" {
-			return nsEnv
-		}
-		return runserver.DefaultPoolNamespace
-	}
-	resolvedPoolNamespace := resolvePoolNamespace()
-	poolNamespacedName := types.NamespacedName{
-		Name:      *poolName,
-		Namespace: resolvedPoolNamespace,
-	}
-	poolGroupKind := schema.GroupKind{
-		Group: *poolGroup,
-		Kind:  "InferencePool",
-	}
-	poolGKNN := common.GKNN{
-		NamespacedName: poolNamespacedName,
-		GroupKind:      poolGroupKind,
-	}
-
 	isLeader := &atomic.Bool{}
 	isLeader.Store(false)
 
-	mgr, err := runserver.NewDefaultManager(poolGKNN, cfg, metricsServerOptions, *haEnableLeaderElection)
+	mgr, err := runserver.NewDefaultManager(disableK8sCrdReconcile, *gknn, cfg, metricsServerOptions, *haEnableLeaderElection)
 	if err != nil {
 		setupLog.Error(err, "Failed to create controller manager")
 		return err
@@ -341,13 +335,7 @@ func (r *Runner) Run(ctx context.Context) error {
 		if err != nil {
 			return fmt.Errorf("failed to initialize Flow Registry: %w", err)
 		}
-		fc, err := fccontroller.NewFlowController(
-			ctx,
-			fcCfg.Controller,
-			registry,
-			saturationDetector,
-			setupLog,
-		)
+		fc, err := fccontroller.NewFlowController(ctx, fcCfg.Controller, registry, saturationDetector, setupLog)
 		if err != nil {
 			return fmt.Errorf("failed to initialize Flow Controller: %w", err)
 		}
@@ -359,7 +347,7 @@ func (r *Runner) Run(ctx context.Context) error {
 	}
 
 	director := requestcontrol.NewDirectorWithConfig(
-		datastore,
+		ds,
 		scheduler,
 		admissionController,
 		r.requestControlConfig)
@@ -367,9 +355,9 @@ func (r *Runner) Run(ctx context.Context) error {
 	// --- Setup ExtProc Server Runner ---
 	serverRunner := &runserver.ExtProcServerRunner{
 		GrpcPort:                         *grpcPort,
-		PoolNamespacedName:               poolNamespacedName,
-		PoolGKNN:                         poolGKNN,
-		Datastore:                        datastore,
+		GKNN:                             *gknn,
+		Datastore:                        ds,
+		DisableK8sCrdReconcile:           disableK8sCrdReconcile,
 		SecureServing:                    *secureServing,
 		HealthChecking:                   *healthChecking,
 		CertPath:                         *certPath,
@@ -386,7 +374,7 @@ func (r *Runner) Run(ctx context.Context) error {
 
 	// --- Add Runnables to Manager ---
 	// Register health server.
-	if err := registerHealthServer(mgr, ctrl.Log.WithName("health"), datastore, *grpcHealthPort, isLeader, *haEnableLeaderElection); err != nil {
+	if err := registerHealthServer(mgr, ctrl.Log.WithName("health"), ds, *grpcHealthPort, isLeader, *haEnableLeaderElection); err != nil {
 		return err
 	}
 
@@ -406,6 +394,28 @@ func (r *Runner) Run(ctx context.Context) error {
 	return nil
 }
 
+func setupDatastore(setupLog logr.Logger, ctx context.Context, epFactory datalayer.EndpointFactory, modelServerMetricsPort int32, disableK8sCrdReconcile bool, namespace, name, endpointSelector, endpointTargetPorts string) (datastore.Datastore, error) {
+	if !disableK8sCrdReconcile {
+		return datastore.NewDatastore(ctx, epFactory, modelServerMetricsPort), nil
+	} else {
+		endpointPool := datalayer.NewEndpointPool(namespace, name)
+		labelsMap, err := labels.ConvertSelectorToLabelsMap(endpointSelector)
+		if err != nil {
+			setupLog.Error(err, "Failed to parse flag %q with error: %w", "endpoint-selector", err)
+			return nil, err
+		}
+		endpointPool.Selector = labelsMap
+		endpointPool.TargetPorts, err = strToUniqueIntSlice(endpointTargetPorts)
+		if err != nil {
+			setupLog.Error(err, "Failed to parse flag %q with error: %w", "endpoint-target-ports", err)
+			return nil, err
+		}
+
+		endpointPoolOption := datastore.WithEndpointPool(endpointPool)
+		return datastore.NewDatastore(ctx, epFactory, modelServerMetricsPort, endpointPoolOption), nil
+	}
+}
+
 // registerInTreePlugins registers the factory functions of all known plugins
 func (r *Runner) registerInTreePlugins() {
 	plugins.Register(prefix.PrefixCachePluginType, prefix.PrefixCachePluginFactory)
@@ -418,11 +428,13 @@ func (r *Runner) registerInTreePlugins() {
 	plugins.Register(scorer.LoraAffinityScorerType, scorer.LoraAffinityScorerFactory)
 	// register filter for test purpose only (used in conformance tests)
 	plugins.Register(testfilter.HeaderBasedTestingFilterType, testfilter.HeaderBasedTestingFilterFactory)
+	// register response received plugin for test purpose only (used in conformance tests)
+	plugins.Register(testresponsereceived.DestinationEndpointServedVerifierType, testresponsereceived.DestinationEndpointServedVerifierFactory)
 }
 
-func (r *Runner) parseConfigurationPhaseOne(ctx context.Context) (*configapi.EndpointPickerConfig, config.FeatureConfig, error) {
+func (r *Runner) parseConfigurationPhaseOne(ctx context.Context) (*configapi.EndpointPickerConfig, error) {
 	if *configText == "" && *configFile == "" {
-		return nil, nil, nil // configuring through code, not through file
+		return nil, nil // configuring through code, not through file
 	}
 
 	logger := log.FromContext(ctx)
@@ -434,7 +446,7 @@ func (r *Runner) parseConfigurationPhaseOne(ctx context.Context) (*configapi.End
 		var err error
 		configBytes, err = os.ReadFile(*configFile)
 		if err != nil {
-			return nil, nil, fmt.Errorf("failed to load config from a file '%s' - %w", *configFile, err)
+			return nil, fmt.Errorf("failed to load config from a file '%s' - %w", *configFile, err)
 		}
 	}
 
@@ -443,7 +455,14 @@ func (r *Runner) parseConfigurationPhaseOne(ctx context.Context) (*configapi.End
 
 	r.registerInTreePlugins()
 
-	return loader.LoadConfigPhaseOne(configBytes, logger)
+	rawConfig, featureGates, err := loader.LoadConfigPhaseOne(configBytes, logger)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse config - %w", err)
+	}
+
+	r.featureGates = featureGates
+
+	return rawConfig, nil
 }
 
 func (r *Runner) parseConfigurationPhaseTwo(ctx context.Context, rawConfig *configapi.EndpointPickerConfig, ds datastore.Datastore) (*config.Config, error) {
@@ -471,18 +490,18 @@ func (r *Runner) deprecatedConfigurationHelper(cfg *config.Config, logger logr.L
 	// Handle deprecated environment variable based feature flags
 
 	if _, ok := os.LookupEnv(enableExperimentalDatalayerV2); ok {
-		logger.Info("Enabling the experimental Data Layer V2 using environment variables is deprecated")
+		logger.Info("Enabling the experimental Data Layer V2 using environment variables is deprecated and will be removed in next version")
 		r.featureGates[datalayer.FeatureGate] = env.GetEnvBool(enableExperimentalDatalayerV2, false, logger)
 	}
 	if _, ok := os.LookupEnv(enableExperimentalFlowControlLayer); ok {
-		logger.Info("Enabling the experimental Flow Control layer using environment variables is deprecated")
+		logger.Info("Enabling the experimental Flow Control layer using environment variables is deprecated and will be removed in next version")
 		r.featureGates[flowcontrol.FeatureGate] = env.GetEnvBool(enableExperimentalFlowControlLayer, false, setupLog)
 	}
 
 	// Handle deprecated environment variable base Saturation Detector configuration
 
 	if _, ok := os.LookupEnv(EnvSdQueueDepthThreshold); ok {
-		logger.Info("Configuring Saturation Detector using environment variables is deprecated")
+		logger.Info("Configuring Saturation Detector using environment variables is deprecated and will be removed in next version")
 		cfg.SaturationDetectorConfig.QueueDepthThreshold =
 			env.GetEnvInt(EnvSdQueueDepthThreshold, saturationdetector.DefaultQueueDepthThreshold, logger)
 		if cfg.SaturationDetectorConfig.QueueDepthThreshold <= 0 {
@@ -490,14 +509,14 @@ func (r *Runner) deprecatedConfigurationHelper(cfg *config.Config, logger logr.L
 		}
 	}
 	if _, ok := os.LookupEnv(EnvSdKVCacheUtilThreshold); ok {
-		logger.Info("Configuring Saturation Detector using environment variables is deprecated")
+		logger.Info("Configuring Saturation Detector using environment variables is deprecated and will be removed in next version")
 		cfg.SaturationDetectorConfig.KVCacheUtilThreshold = env.GetEnvFloat(EnvSdKVCacheUtilThreshold, saturationdetector.DefaultKVCacheUtilThreshold, logger)
 		if cfg.SaturationDetectorConfig.KVCacheUtilThreshold <= 0 || cfg.SaturationDetectorConfig.KVCacheUtilThreshold >= 1 {
 			cfg.SaturationDetectorConfig.KVCacheUtilThreshold = saturationdetector.DefaultKVCacheUtilThreshold
 		}
 	}
 	if _, ok := os.LookupEnv(EnvSdMetricsStalenessThreshold); ok {
-		logger.Info("Configuring Saturation Detector using environment variables is deprecated")
+		logger.Info("Configuring Saturation Detector using environment variables is deprecated and will be removed in next version")
 		cfg.SaturationDetectorConfig.MetricsStalenessThreshold = env.GetEnvDuration(EnvSdMetricsStalenessThreshold, saturationdetector.DefaultMetricsStalenessThreshold, logger)
 		if cfg.SaturationDetectorConfig.MetricsStalenessThreshold <= 0 {
 			cfg.SaturationDetectorConfig.MetricsStalenessThreshold = saturationdetector.DefaultMetricsStalenessThreshold
@@ -635,9 +654,19 @@ func registerHealthServer(mgr manager.Manager, logger logr.Logger, ds datastore.
 }
 
 func validateFlags() error {
-	if *poolName == "" {
-		return fmt.Errorf("required %q flag not set", "poolName")
+	if (*poolName != "" && *endpointSelector != "") || (*poolName == "" && *endpointSelector == "") {
+		return errors.New("either pool-name or endpoint-selector must be set")
 	}
+	if *endpointSelector != "" {
+		targetPortsList, err := strToUniqueIntSlice(*endpointTargetPorts)
+		if err != nil {
+			return fmt.Errorf("unexpected value for %q flag with error %w", "endpoint-target-ports", err)
+		}
+		if len(targetPortsList) == 0 || len(targetPortsList) > 8 {
+			return fmt.Errorf("flag %q should have length from 1 to 8", "endpoint-target-ports")
+		}
+	}
+
 	if *configText != "" && *configFile != "" {
 		return fmt.Errorf("both the %q and %q flags can not be set at the same time", "configText", "configFile")
 	}
@@ -648,6 +677,34 @@ func validateFlags() error {
 	return nil
 }
 
+func strToUniqueIntSlice(s string) ([]int, error) {
+	seen := sets.NewInt()
+	var intList []int
+
+	if s == "" {
+		return intList, nil
+	}
+
+	strList := strings.Split(s, ",")
+
+	for _, str := range strList {
+		trimmedStr := strings.TrimSpace(str)
+		if trimmedStr == "" {
+			continue
+		}
+		portInt, err := strconv.Atoi(trimmedStr)
+		if err != nil {
+			return nil, fmt.Errorf("invalid number: '%s' is not an integer", trimmedStr)
+		}
+
+		if _, ok := seen[portInt]; !ok {
+			seen[portInt] = struct{}{}
+			intList = append(intList, portInt)
+		}
+	}
+	return intList, nil
+}
+
 func verifyMetricMapping(mapping backendmetrics.MetricMapping, logger logr.Logger) {
 	if mapping.TotalQueuedRequests == nil {
 		logger.Info("Not scraping metric: TotalQueuedRequests")
@@ -683,3 +740,62 @@ func setupPprofHandlers(mgr ctrl.Manager) error {
 	}
 	return nil
 }
+
+func extractDeploymentName(podName string) (string, error) {
+	regex := regexp.MustCompile(`^(.+)-[a-z0-9]+-[a-z0-9]+$`)
+
+	matches := regex.FindStringSubmatch(podName)
+	if len(matches) == 2 {
+		return matches[1], nil
+	}
+	return "", fmt.Errorf("failed to parse deployment name from pod name %s", podName)
+}
+
+func extractGKNN(poolName, poolGroup, poolNamespace, endpointSelector string) (*common.GKNN, error) {
+	if poolName != "" {
+		// Determine pool namespace: if --pool-namespace is non-empty, use it; else NAMESPACE env var; else default
+		resolvedPoolNamespace := resolvePoolNamespace(poolNamespace)
+		poolNamespacedName := types.NamespacedName{
+			Name:      poolName,
+			Namespace: resolvedPoolNamespace,
+		}
+		poolGroupKind := schema.GroupKind{
+			Group: poolGroup,
+			Kind:  "InferencePool",
+		}
+		return &common.GKNN{
+			NamespacedName: poolNamespacedName,
+			GroupKind:      poolGroupKind,
+		}, nil
+	}
+
+	if endpointSelector != "" {
+		// Determine EPP namespace: NAMESPACE env var; else default
+		resolvedPoolNamespace := resolvePoolNamespace(poolNamespace)
+		// Determine EPP name: POD_NAME env var
+		eppPodNameEnv := os.Getenv("POD_NAME")
+		if eppPodNameEnv == "" {
+			return nil, errors.New("failed to get environment variable POD_NAME")
+
+		}
+		eppName, err := extractDeploymentName(eppPodNameEnv)
+		if err != nil {
+			return nil, err
+		}
+		return &common.GKNN{
+			NamespacedName: types.NamespacedName{Namespace: resolvedPoolNamespace, Name: eppName},
+			GroupKind:      schema.GroupKind{Kind: "Deployment", Group: "apps"},
+		}, nil
+	}
+	return nil, errors.New("can't construct gknn as both pool-name and endpoint-selector are missing")
+}
+
+func resolvePoolNamespace(poolNamespace string) string {
+	if poolNamespace != "" {
+		return poolNamespace
+	}
+	if nsEnv := os.Getenv("NAMESPACE"); nsEnv != "" {
+		return nsEnv
+	}
+	return runserver.DefaultPoolNamespace
+}
diff --git a/config/charts/inferencepool/README.md b/config/charts/inferencepool/README.md
index da4a1a9..cf48fca 100644
--- a/config/charts/inferencepool/README.md
+++ b/config/charts/inferencepool/README.md
@@ -225,16 +225,6 @@ The following table list the configurable parameters of the chart.
 
 This section should document any Gateway provider specific values configurations.
 
-#### GKE
-
-These are the options available to you with `provider.name` set to `gke`:
-
-| **Parameter Name**                          | **Description**                                                                                                        |
-|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
-| `gke.monitoringSecret.name`                 | The name of the monitoring secret to be used. Defaults to `inference-gateway-sa-metrics-reader-secret`.                |
-| `gke.monitoringSecret.namespace`            | The namespace that the monitoring secret lives in. Defaults to `default`.                                              |
-
-
 #### Istio
 
 These are the options available to you with `provider.name` set to `istio`:
diff --git a/config/charts/inferencepool/templates/epp-deployment.yaml b/config/charts/inferencepool/templates/epp-deployment.yaml
index 5b3634c..be6a39e 100644
--- a/config/charts/inferencepool/templates/epp-deployment.yaml
+++ b/config/charts/inferencepool/templates/epp-deployment.yaml
@@ -147,6 +147,10 @@ spec:
           valueFrom:
             fieldRef:
               fieldPath: metadata.namespace
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
         {{- if .Values.inferenceExtension.tracing.enabled }}
         - name: OTEL_SERVICE_NAME
           value: "gateway-api-inference-extension"
diff --git a/mkdocs.yml b/mkdocs.yml
index 9fca3cb..d0998c6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -84,6 +84,8 @@ nav:
     - Benchmark: performance/benchmark/index.md
     - Advanced Benchmarking Configs: 
       - Prefix Cache Aware: performance/benchmark/advanced-configs/prefix-cache-aware.md
+      - Decode Heavy Workload: performance/benchmark/advanced-configs/decode-heavy-workload.md
+      - Prefill Heavy Workload: performance/benchmark/advanced-configs/prefill-heavy-workload.md
     - Regression Testing: performance/regression-testing/index.md
   - Reference:
     - v1 API Reference: reference/spec.md
diff --git a/pkg/epp/config/config.go b/pkg/epp/config/config.go
index 59a5962..d966e3c 100644
--- a/pkg/epp/config/config.go
+++ b/pkg/epp/config/config.go
@@ -26,5 +26,3 @@ type Config struct {
 	SchedulerConfig          *scheduling.SchedulerConfig
 	SaturationDetectorConfig *saturationdetector.Config
 }
-
-type FeatureConfig map[string]bool
diff --git a/pkg/epp/config/loader/configloader.go b/pkg/epp/config/loader/configloader.go
index 3488b25..56f05dc 100644
--- a/pkg/epp/config/loader/configloader.go
+++ b/pkg/epp/config/loader/configloader.go
@@ -37,14 +37,14 @@ import (
 
 var scheme = runtime.NewScheme()
 
-var registeredFeatureGates = map[string]struct{}{}
+var registeredFeatureGates = sets.New[string]() // set of feature gates names, a name must be unique
 
 func init() {
 	utilruntime.Must(configapi.Install(scheme))
 }
 
 // LoadConfigPhaseOne first phase of loading configuration from supplied text that was converted to []byte
-func LoadConfigPhaseOne(configBytes []byte, logger logr.Logger) (*configapi.EndpointPickerConfig, config.FeatureConfig, error) {
+func LoadConfigPhaseOne(configBytes []byte, logger logr.Logger) (*configapi.EndpointPickerConfig, map[string]bool, error) {
 	rawConfig, err := loadRawConfig(configBytes)
 	if err != nil {
 		return nil, nil, err
@@ -201,5 +201,5 @@ func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle plugins
 
 // RegisterFeatureGate registers feature gate keys for validation
 func RegisterFeatureGate(gate string) {
-	registeredFeatureGates[gate] = struct{}{}
+	registeredFeatureGates.Insert(gate)
 }
diff --git a/pkg/epp/config/loader/validation.go b/pkg/epp/config/loader/validation.go
index e43fcb5..7eed7ef 100644
--- a/pkg/epp/config/loader/validation.go
+++ b/pkg/epp/config/loader/validation.go
@@ -62,7 +62,7 @@ func validateFeatureGates(fg configapi.FeatureGates) error {
 	}
 
 	for _, gate := range fg {
-		if _, ok := registeredFeatureGates[gate]; !ok {
+		if !registeredFeatureGates.Has(gate) {
 			return errors.New(gate + " is an unregistered Feature Gate")
 		}
 	}
diff --git a/pkg/epp/controller/inferenceobjective_reconciler.go b/pkg/epp/controller/inferenceobjective_reconciler.go
index c8ac5a6..49ddf70 100644
--- a/pkg/epp/controller/inferenceobjective_reconciler.go
+++ b/pkg/epp/controller/inferenceobjective_reconciler.go
@@ -21,12 +21,12 @@ import (
 	"fmt"
 
 	"k8s.io/apimachinery/pkg/api/errors"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
-
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
@@ -55,7 +55,7 @@ func (c *InferenceObjectiveReconciler) Reconcile(ctx context.Context, req ctrl.R
 	}
 
 	if notFound || !infObjective.DeletionTimestamp.IsZero() || infObjective.Spec.PoolRef.Name != v1alpha2.ObjectName(c.PoolGKNN.Name) || infObjective.Spec.PoolRef.Group != v1alpha2.Group(c.PoolGKNN.Group) {
-		// InferenceObjective object got deleted or changed the referenced pool.
+		// InferenceObjective object got deleted or changed the referenced inferencePool.
 		c.Datastore.ObjectiveDelete(req.NamespacedName)
 		return ctrl.Result{}, nil
 	}
diff --git a/pkg/epp/controller/inferencepool_reconciler.go b/pkg/epp/controller/inferencepool_reconciler.go
index 3b52de0..400ce43 100644
--- a/pkg/epp/controller/inferencepool_reconciler.go
+++ b/pkg/epp/controller/inferencepool_reconciler.go
@@ -21,20 +21,22 @@ import (
 	"fmt"
 
 	"k8s.io/apimachinery/pkg/api/errors"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/log"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	pooltuil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 )
 
 // InferencePoolReconciler utilizes the controller runtime to reconcile Instance Gateway resources
 // 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.
+// will have the proper controller that will create/manage objects on behalf of the server inferencePool.
 type InferencePoolReconciler struct {
 	client.Reader
 	Datastore datastore.Datastore
@@ -75,25 +77,17 @@ func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		c.Datastore.Clear()
 		return ctrl.Result{}, nil
 	}
-
-	// 4. Convert the fetched object to the canonical v1.InferencePool.
-	v1infPool := &v1.InferencePool{}
-
+	var endpointPool *datalayer.EndpointPool
 	switch pool := obj.(type) {
 	case *v1.InferencePool:
-		// If it's already a v1 object, just use it.
-		v1infPool = pool
+		endpointPool = pooltuil.InferencePoolToEndpointPool(pool)
 	case *v1alpha2.InferencePool:
-		var err error
-		err = pool.ConvertTo(v1infPool)
-		if err != nil {
-			return ctrl.Result{}, fmt.Errorf("failed to convert XInferencePool to InferencePool - %w", err)
-		}
+		endpointPool = pooltuil.AlphaInferencePoolToEndpointPool(pool)
 	default:
 		return ctrl.Result{}, fmt.Errorf("unsupported API group: %s", c.PoolGKNN.Group)
 	}
 
-	if err := c.Datastore.PoolSet(ctx, c.Reader, v1infPool); err != nil {
+	if err := c.Datastore.PoolSet(ctx, c.Reader, endpointPool); err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to update datastore - %w", err)
 	}
 
diff --git a/pkg/epp/controller/pod_reconciler.go b/pkg/epp/controller/pod_reconciler.go
index b3a78ef..06be5d7 100644
--- a/pkg/epp/controller/pod_reconciler.go
+++ b/pkg/epp/controller/pod_reconciler.go
@@ -23,12 +23,12 @@ import (
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
-
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 	podutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pod"
diff --git a/pkg/epp/datalayer/endpoint_pool.go b/pkg/epp/datalayer/endpoint_pool.go
new file mode 100644
index 0000000..05d91d4
--- /dev/null
+++ b/pkg/epp/datalayer/endpoint_pool.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 datalayer
+
+type EndpointPool struct {
+	Selector    map[string]string
+	TargetPorts []int
+	Namespace   string
+	Name        string
+}
+
+// NewEndpointPool creates and returns a new empty instance of EndpointPool.
+func NewEndpointPool(namespace string, name string) *EndpointPool {
+	return &EndpointPool{
+		Selector:    make(map[string]string),
+		TargetPorts: []int{},
+		Namespace:   namespace,
+		Name:        name,
+	}
+}
diff --git a/pkg/epp/datalayer/factory.go b/pkg/epp/datalayer/factory.go
index 58da604..3a81763 100644
--- a/pkg/epp/datalayer/factory.go
+++ b/pkg/epp/datalayer/factory.go
@@ -23,8 +23,6 @@ import (
 
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/log"
-
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 )
 
 const (
@@ -40,7 +38,7 @@ const (
 //   - Global metrics logging uses PoolGet solely for error return and PodList to enumerate
 //     all endpoints for metrics summarization.
 type PoolInfo interface {
-	PoolGet() (*v1.InferencePool, error)
+	PoolGet() (*EndpointPool, error)
 	PodList(func(Endpoint) bool) []Endpoint
 }
 
diff --git a/pkg/epp/datastore/datastore.go b/pkg/epp/datastore/datastore.go
index 5d6ff75..2ab2e98 100644
--- a/pkg/epp/datastore/datastore.go
+++ b/pkg/epp/datastore/datastore.go
@@ -28,10 +28,9 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
+
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/log"
-
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
@@ -49,8 +48,8 @@ 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, reader client.Reader, pool *v1.InferencePool) error
-	PoolGet() (*v1.InferencePool, error)
+	PoolSet(ctx context.Context, reader client.Reader, endpointPool *datalayer.EndpointPool) error
+	PoolGet() (*datalayer.EndpointPool, error)
 	PoolHasSynced() bool
 	PoolLabelsMatch(podLabels map[string]string) bool
 
@@ -69,15 +68,23 @@ type Datastore interface {
 	Clear()
 }
 
-func NewDatastore(parentCtx context.Context, epFactory datalayer.EndpointFactory, modelServerMetricsPort int32) Datastore {
+func NewDatastore(parentCtx context.Context, epFactory datalayer.EndpointFactory, modelServerMetricsPort int32, opts ...DatastoreOption) Datastore {
+	// Initialize with defaults
 	store := &datastore{
 		parentCtx:              parentCtx,
 		poolAndObjectivesMu:    sync.RWMutex{},
+		pool:                   nil,
 		objectives:             make(map[string]*v1alpha2.InferenceObjective),
 		pods:                   &sync.Map{},
 		modelServerMetricsPort: modelServerMetricsPort,
 		epf:                    epFactory,
 	}
+
+	// Apply options
+	for _, opt := range opts {
+		opt(store)
+	}
+
 	return store
 }
 
@@ -86,8 +93,8 @@ type datastore struct {
 	parentCtx context.Context
 	// poolAndObjectivesMu is used to synchronize access to pool and the objectives map.
 	poolAndObjectivesMu sync.RWMutex
-	pool                *v1.InferencePool
-	// key: InferenceObjective name, value: *InferenceObjective
+	pool                *datalayer.EndpointPool
+	// key: InferenceObjective.Spec.ModelName, value: *InferenceObjective
 	objectives map[string]*v1alpha2.InferenceObjective
 	// key: types.NamespacedName, value: backendmetrics.PodMetrics
 	pods *sync.Map
@@ -110,9 +117,9 @@ func (ds *datastore) Clear() {
 	ds.pods.Clear()
 }
 
-// /// InferencePool APIs ///
-func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, pool *v1.InferencePool) error {
-	if pool == nil {
+// /// Pool APIs ///
+func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, endpointPool *datalayer.EndpointPool) error {
+	if endpointPool == nil {
 		ds.Clear()
 		return nil
 	}
@@ -120,10 +127,10 @@ func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, pool *v1
 	ds.poolAndObjectivesMu.Lock()
 	defer ds.poolAndObjectivesMu.Unlock()
 
-	oldPool := ds.pool
-	ds.pool = pool
-	if oldPool == nil || !reflect.DeepEqual(pool.Spec.Selector, oldPool.Spec.Selector) {
-		logger.V(logutil.DEFAULT).Info("Updating inference pool endpoints", "selector", pool.Spec.Selector)
+	oldEndpointPool := ds.pool
+	ds.pool = endpointPool
+	if oldEndpointPool == nil || !reflect.DeepEqual(oldEndpointPool.Selector, endpointPool.Selector) {
+		logger.V(logutil.DEFAULT).Info("Updating endpoints", "selector", endpointPool.Selector)
 		// A full resync is required to address two cases:
 		// 1) At startup, the pod events may get processed before the pool is synced with the datastore,
 		//    and hence they will not be added to the store since pool selector is not known yet
@@ -138,7 +145,7 @@ func (ds *datastore) PoolSet(ctx context.Context, reader client.Reader, pool *v1
 	return nil
 }
 
-func (ds *datastore) PoolGet() (*v1.InferencePool, error) {
+func (ds *datastore) PoolGet() (*datalayer.EndpointPool, error) {
 	ds.poolAndObjectivesMu.RLock()
 	defer ds.poolAndObjectivesMu.RUnlock()
 	if !ds.PoolHasSynced() {
@@ -159,7 +166,7 @@ func (ds *datastore) PoolLabelsMatch(podLabels map[string]string) bool {
 	if ds.pool == nil {
 		return false
 	}
-	poolSelector := selectorFromInferencePoolSelector(ds.pool.Spec.Selector.MatchLabels)
+	poolSelector := labels.SelectorFromSet(ds.pool.Selector)
 	podSet := labels.Set(podLabels)
 	return poolSelector.Matches(podSet)
 }
@@ -225,14 +232,14 @@ func (ds *datastore) PodUpdateOrAddIfNotExist(pod *corev1.Pod) bool {
 	}
 
 	modelServerMetricsPort := 0
-	if len(ds.pool.Spec.TargetPorts) == 1 {
+	if len(ds.pool.TargetPorts) == 1 {
 		modelServerMetricsPort = int(ds.modelServerMetricsPort)
 	}
 	pods := []*datalayer.PodInfo{}
-	for idx, port := range ds.pool.Spec.TargetPorts {
+	for idx, port := range ds.pool.TargetPorts {
 		metricsPort := modelServerMetricsPort
 		if metricsPort == 0 {
-			metricsPort = int(port.Number)
+			metricsPort = port
 		}
 		pods = append(pods,
 			&datalayer.PodInfo{
@@ -242,7 +249,7 @@ func (ds *datastore) PodUpdateOrAddIfNotExist(pod *corev1.Pod) bool {
 				},
 				PodName:     pod.Name,
 				Address:     pod.Status.PodIP,
-				Port:        strconv.Itoa(int(port.Number)),
+				Port:        strconv.Itoa(port),
 				MetricsHost: net.JoinHostPort(pod.Status.PodIP, strconv.Itoa(metricsPort)),
 				Labels:      labels,
 			})
@@ -280,7 +287,7 @@ func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) err
 	logger := log.FromContext(ctx)
 	podList := &corev1.PodList{}
 	if err := reader.List(ctx, podList, &client.ListOptions{
-		LabelSelector: selectorFromInferencePoolSelector(ds.pool.Spec.Selector.MatchLabels),
+		LabelSelector: labels.SelectorFromSet(ds.pool.Selector),
 		Namespace:     ds.pool.Namespace,
 	}); err != nil {
 		return fmt.Errorf("failed to list pods - %w", err)
@@ -313,14 +320,10 @@ func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) err
 	return nil
 }
 
-func selectorFromInferencePoolSelector(selector map[v1.LabelKey]v1.LabelValue) labels.Selector {
-	return labels.SelectorFromSet(stripLabelKeyAliasFromLabelMap(selector))
-}
+type DatastoreOption func(*datastore)
 
-func stripLabelKeyAliasFromLabelMap(labels map[v1.LabelKey]v1.LabelValue) map[string]string {
-	outMap := make(map[string]string)
-	for k, v := range labels {
-		outMap[string(k)] = string(v)
+func WithEndpointPool(pool *datalayer.EndpointPool) DatastoreOption {
+	return func(d *datastore) {
+		d.pool = pool
 	}
-	return outMap
 }
diff --git a/pkg/epp/flowcontrol/controller/internal/processor.go b/pkg/epp/flowcontrol/controller/internal/processor.go
index 7fb74aa..489f421 100644
--- a/pkg/epp/flowcontrol/controller/internal/processor.go
+++ b/pkg/epp/flowcontrol/controller/internal/processor.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"runtime"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -165,6 +164,10 @@ func (sp *ShardProcessor) Run(ctx context.Context) {
 	sp.wg.Add(1)
 	go sp.runCleanupSweep(ctx)
 
+	// Create a ticker for periodic dispatch attempts to avoid tight loops
+	dispatchTicker := sp.clock.NewTicker(time.Millisecond)
+	defer dispatchTicker.Stop()
+
 	// This is the main worker loop. It continuously processes incoming requests and dispatches queued requests until the
 	// context is cancelled. The `select` statement has three cases:
 	//
@@ -172,9 +175,8 @@ func (sp *ShardProcessor) Run(ctx context.Context) {
 	//     loop will drain all queues and exit. This is the primary exit condition.
 	//  2. New Item Arrival: If an item is available on `enqueueChan`, it will be processed. This ensures that the
 	//     processor is responsive to new work.
-	//  3. Default (Dispatch): If neither of the above cases is ready, the `default` case executes, ensuring the loop is
-	//     non-blocking. It continuously attempts to dispatch items from the existing backlog, preventing starvation and
-	//     ensuring queues are drained.
+	//  3. Dispatch Ticker: Periodically triggers a dispatch cycle to attempt to dispatch items from existing queues,
+	//     ensuring that queued work is processed even when no new items arrive.
 	for {
 		select {
 		case <-ctx.Done():
@@ -193,14 +195,9 @@ func (sp *ShardProcessor) Run(ctx context.Context) {
 				continue
 			}
 			sp.enqueue(item)
-			sp.dispatchCycle(ctx)
-		default:
-			// If no new items are arriving, continuously try to dispatch from the backlog.
-			if !sp.dispatchCycle(ctx) {
-				// If no work was done, yield to the scheduler to prevent a tight, busy-loop when idle, while still allowing for
-				// immediate rescheduling.
-				runtime.Gosched()
-			}
+			sp.dispatchCycle(ctx) // Process immediately when an item arrives
+		case <-dispatchTicker.C():
+			sp.dispatchCycle(ctx) // Periodically attempt to dispatch from queues
 		}
 	}
 }
diff --git a/pkg/epp/handlers/server.go b/pkg/epp/handlers/server.go
index 5296d49..a67a1ea 100644
--- a/pkg/epp/handlers/server.go
+++ b/pkg/epp/handlers/server.go
@@ -29,10 +29,10 @@ import (
 	"github.com/google/uuid"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
-	"sigs.k8s.io/controller-runtime/pkg/log"
 
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
@@ -61,7 +61,7 @@ type Director interface {
 }
 
 type Datastore interface {
-	PoolGet() (*v1.InferencePool, error)
+	PoolGet() (*datalayer.EndpointPool, error)
 }
 
 // Server implements the Envoy external processing server.
diff --git a/pkg/epp/metadata/consts.go b/pkg/epp/metadata/consts.go
index be6c482..b69319b 100644
--- a/pkg/epp/metadata/consts.go
+++ b/pkg/epp/metadata/consts.go
@@ -26,6 +26,8 @@ const (
 	DestinationEndpointNamespace = "envoy.lb"
 	// DestinationEndpointKey is the header and response metadata key used by Envoy to route to the appropriate pod.
 	DestinationEndpointKey = "x-gateway-destination-endpoint"
+	// DestinationEndpointServedKey is the metadata key used by Envoy to specify the endpoint that served the request.
+	DestinationEndpointServedKey = "x-gateway-destination-endpoint-served"
 	// FlowFairnessIDKey is the header key used to pass the fairness ID to be used in Flow Control.
 	FlowFairnessIDKey = "x-gateway-inference-fairness-id"
 	// ObjectiveKey is the header key used to specify the objective of an incoming request.
diff --git a/pkg/epp/metrics/metrics.go b/pkg/epp/metrics/metrics.go
index 59c8976..4958c59 100644
--- a/pkg/epp/metrics/metrics.go
+++ b/pkg/epp/metrics/metrics.go
@@ -23,9 +23,9 @@ import (
 
 	"github.com/prometheus/client_golang/prometheus"
 	compbasemetrics "k8s.io/component-base/metrics"
+
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/metrics"
-
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 	metricsutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/metrics"
 )
@@ -63,6 +63,104 @@ var (
 		[]string{"model_name", "target_model_name", "error_code"},
 	)
 
+	// Gauge for various inference request metrics
+	inferenceGauges = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "inference_request_metric",
+			Help:      metricsutil.HelpMsgWithStability("Consolidated gauge for various inference request metrics including TTFT, TPOT, SLOs, and prediction durations.", compbasemetrics.ALPHA),
+		},
+		[]string{"model_name", "target_model_name", "type"},
+	)
+
+	requestTTFT = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_ttft_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Inference model TTFT distribution in seconds for each model and target model.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.005, 0.025, 0.05, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0, 1.25, 1.5, 2, 3,
+				4, 5, 6, 8, 10, 15, 20, 30, 45, 60, 120, 180, 240, 300, 360, 480, 600, 900, 1200, 1800, 2700, 3600,
+			},
+		},
+		[]string{"model_name", "target_model_name"},
+	)
+
+	requestPredictedTTFT = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_predicted_ttft_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Inference model Predicted TTFT distribution in seconds for each model and target model.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.005, 0.025, 0.05, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0, 1.25, 1.5, 2, 3,
+				4, 5, 6, 8, 10, 15, 20, 30, 45, 60, 120, 180, 240, 300, 360, 480, 600, 900, 1200, 1800, 2700, 3600,
+			},
+		},
+		[]string{"model_name", "target_model_name"},
+	)
+
+	// New metrics for TTFT prediction duration
+	requestTTFTPredictionDuration = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_ttft_prediction_duration_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Duration taken to generate TTFT predictions in seconds for each model and target model.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0,
+			},
+		},
+		[]string{"model_name", "target_model_name"},
+	)
+
+	requestTPOT = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_tpot_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Inference model TPOT distribution in seconds for each model and target model.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.0005, 0.00205, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.125, 0.15, 0.2, 0.3,
+				0.4, 0.5, 0.6, 0.8, 1, 1.5, 2, 3, 4.5, 6, 12, 18, 24, 30, 36, 48, 60, 90, 120, 180, 270, 360,
+			},
+		},
+		[]string{"model_name", "target_model_name"},
+	)
+
+	requestPredictedTPOT = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_predicted_tpot_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Inference model Predicted TPOT distribution in seconds for each model and target model.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.0005, 0.00205, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.125, 0.15, 0.2, 0.3,
+				0.4, 0.5, 0.6, 0.8, 1, 1.5, 2, 3, 4.5, 6, 12, 18, 24, 30, 36, 48, 60, 90, 120, 180, 270, 360,
+			},
+		},
+		[]string{"model_name", "target_model_name"},
+	)
+
+	// New metrics for TPOT prediction duration
+	requestTPOTPredictionDuration = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_tpot_prediction_duration_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Duration taken to generate TPOT predictions in seconds for each model and target model.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0,
+			},
+		},
+		[]string{"model_name", "target_model_name"},
+	)
+
+	// Counter for SLO Violations
+	sloViolationCounter = prometheus.NewCounterVec(
+		prometheus.CounterOpts{
+			Subsystem: InferenceObjectiveComponent,
+			Name:      "request_slo_violation_total",
+			Help:      metricsutil.HelpMsgWithStability("Counter of SLO violations for each model, target model, and violation type.", compbasemetrics.ALPHA),
+		},
+		[]string{"model_name", "target_model_name", "type"},
+	)
+
 	requestLatencies = prometheus.NewHistogramVec(
 		prometheus.HistogramOpts{
 			Subsystem: InferenceObjectiveComponent,
@@ -282,6 +380,21 @@ var registerMetrics sync.Once
 // Register all metrics.
 func Register(customCollectors ...prometheus.Collector) {
 	registerMetrics.Do(func() {
+		// Register inference gauges
+		metrics.Registry.MustRegister(inferenceGauges)
+
+		// Register Histograms
+		metrics.Registry.MustRegister(requestTPOT)
+		metrics.Registry.MustRegister(requestTTFT)
+		metrics.Registry.MustRegister(requestPredictedTPOT)
+		metrics.Registry.MustRegister(requestPredictedTTFT)
+		metrics.Registry.MustRegister(requestTPOTPredictionDuration)
+		metrics.Registry.MustRegister(requestTTFTPredictionDuration)
+
+		// Register SLO violation counters
+		metrics.Registry.MustRegister(sloViolationCounter)
+
+		// Register other metrics
 		metrics.Registry.MustRegister(requestCounter)
 		metrics.Registry.MustRegister(requestErrCounter)
 		metrics.Registry.MustRegister(requestLatencies)
@@ -311,6 +424,21 @@ func Register(customCollectors ...prometheus.Collector) {
 
 // Just for integration test
 func Reset() {
+	// Reset inference gauges
+	inferenceGauges.Reset()
+
+	// Reset Histograms
+	requestTPOT.Reset()
+	requestTTFT.Reset()
+	requestPredictedTPOT.Reset()
+	requestPredictedTTFT.Reset()
+	requestTPOTPredictionDuration.Reset()
+	requestTTFTPredictionDuration.Reset()
+
+	// Reset SLO violation counter
+	sloViolationCounter.Reset()
+
+	// Reset other metrics
 	requestCounter.Reset()
 	requestErrCounter.Reset()
 	requestLatencies.Reset()
@@ -363,6 +491,123 @@ func RecordRequestLatencies(ctx context.Context, modelName, targetModelName stri
 	return true
 }
 
+func RecordRequestTPOT(ctx context.Context, modelName, targetModelName string, tpot float64) bool {
+	if tpot < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "TPOT value must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "tpot", tpot)
+		return false
+	}
+	requestTPOT.WithLabelValues(modelName, targetModelName).Observe(tpot)
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "tpot"}).Set(tpot)
+	return true
+}
+
+// RecordRequestTPOTWithSLO records TPOT and checks for SLO violation.
+// If tpot exceeds the threshold, it records a violation (sets gauge to 1 and increments counter).
+// If tpot is within limits, it sets gauge to 0.
+func RecordRequestTPOTWithSLO(ctx context.Context, modelName, targetModelName string, tpot float64, sloThreshold float64) bool {
+	if tpot < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "TPOT value must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "tpot", tpot)
+		return false
+	}
+
+	// Check for SLO violation (tpot exceeds threshold)
+	if tpot > sloThreshold {
+		inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "tpot_slo_violation"}).Set(1)
+		sloViolationCounter.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "tpot"}).Inc()
+		log.FromContext(ctx).V(logutil.DEFAULT).Info("TPOT SLO violation detected",
+			"modelName", modelName, "targetModelName", targetModelName, "tpot", tpot, "threshold", sloThreshold)
+	} else {
+		inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "tpot_slo_violation"}).Set(0)
+	}
+
+	return true
+}
+
+// TPOT records duration of request.
+func RecordRequestPredictedTPOT(ctx context.Context, modelName, targetModelName string, predicted_tpot float64) bool {
+	if predicted_tpot < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "Predicted TPOT value must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "tpot", predicted_tpot)
+		return false
+	}
+	requestPredictedTPOT.WithLabelValues(modelName, targetModelName).Observe(predicted_tpot)
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "predicted_tpot"}).Set(predicted_tpot)
+	return true
+}
+
+// RecordRequestTPOTPredictionDuration records the duration taken to generate TPOT predictions.
+func RecordRequestTPOTPredictionDuration(ctx context.Context, modelName, targetModelName string, duration float64) bool {
+	if duration < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "TPOT prediction duration must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "duration", duration)
+		return false
+	}
+	requestTPOTPredictionDuration.WithLabelValues(modelName, targetModelName).Observe(duration)
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "tpot_prediction_duration"}).Set(duration)
+	return true
+}
+
+// TTFT records duration of request.
+func RecordRequestTTFT(ctx context.Context, modelName, targetModelName string, ttft float64) bool {
+	if ttft < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "TTFT value must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "ttft", ttft)
+		return false
+	}
+	requestTTFT.WithLabelValues(modelName, targetModelName).Observe(ttft)
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "ttft"}).Set(ttft)
+	return true
+}
+
+// RecordRequestTTFTWithSLO records TTFT and checks for SLO violation.
+// If ttft exceeds the threshold, it records a violation (sets gauge to 1 and increments counter).
+// If ttft is within limits, it sets gauge to 0.
+func RecordRequestTTFTWithSLO(ctx context.Context, modelName, targetModelName string, ttft float64, sloThreshold float64) bool {
+	if ttft < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "TTFT value must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "ttft", ttft)
+		return false
+	}
+
+	// Check for SLO violation (ttft exceeds threshold)
+	if ttft > sloThreshold {
+		inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "ttft_slo_violation"}).Set(1)
+		sloViolationCounter.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "ttft"}).Inc()
+		log.FromContext(ctx).V(logutil.DEFAULT).Info("TTFT SLO violation detected",
+			"modelName", modelName, "targetModelName", targetModelName, "ttft", ttft, "threshold", sloThreshold)
+	} else {
+		inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "ttft_slo_violation"}).Set(0)
+	}
+
+	return true
+}
+
+// TPOT records duration of request.
+func RecordRequestPredictedTTFT(ctx context.Context, modelName, targetModelName string, predicted_ttft float64) bool {
+	if predicted_ttft < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "Predicted TTFT value must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "ttft", predicted_ttft)
+		return false
+	}
+	requestPredictedTTFT.WithLabelValues(modelName, targetModelName).Observe(predicted_ttft)
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "predicted_ttft"}).Set(predicted_ttft)
+	return true
+}
+
+// RecordRequestTTFTPredictionDuration records the duration taken to generate TTFT predictions.
+func RecordRequestTTFTPredictionDuration(ctx context.Context, modelName, targetModelName string, duration float64) bool {
+	if duration < 0 {
+		log.FromContext(ctx).V(logutil.DEFAULT).Error(nil, "TTFT prediction duration must be non-negative",
+			"modelName", modelName, "targetModelName", targetModelName, "duration", duration)
+		return false
+	}
+	requestTTFTPredictionDuration.WithLabelValues(modelName, targetModelName).Observe(duration)
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "ttft_prediction_duration"}).Set(duration)
+	return true
+}
+
 // RecordResponseSizes records the response sizes.
 func RecordResponseSizes(modelName, targetModelName string, size int) {
 	responseSizes.WithLabelValues(modelName, targetModelName).Observe(float64(size))
@@ -480,3 +725,15 @@ func IncFlowControlQueueSize(fairnessID, priority string) {
 func DecFlowControlQueueSize(fairnessID, priority string) {
 	flowControlQueueSize.WithLabelValues(fairnessID, priority).Dec()
 }
+
+// SetTTFTSLOThreshold sets the TTFT SLO threshold for a model.
+// This allows dynamic threshold management and makes the threshold visible in metrics.
+func SetTTFTSLOThreshold(modelName, targetModelName string, threshold float64) {
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "ttft_slo_threshold"}).Set(threshold)
+}
+
+// SetTPOTSLOThreshold sets the TPOT SLO threshold for a model.
+// This allows dynamic threshold management and makes the threshold visible in metrics.
+func SetTPOTSLOThreshold(modelName, targetModelName string, threshold float64) {
+	inferenceGauges.With(prometheus.Labels{"model_name": modelName, "target_model_name": targetModelName, "type": "tpot_slo_threshold"}).Set(threshold)
+}
diff --git a/pkg/epp/requestcontrol/director.go b/pkg/epp/requestcontrol/director.go
index e8db59b..c4f4f1c 100644
--- a/pkg/epp/requestcontrol/director.go
+++ b/pkg/epp/requestcontrol/director.go
@@ -27,8 +27,6 @@ import (
 	"time"
 
 	"sigs.k8s.io/controller-runtime/pkg/log"
-
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
@@ -49,7 +47,7 @@ const (
 
 // Datastore defines the interface required by the Director.
 type Datastore interface {
-	PoolGet() (*v1.InferencePool, error)
+	PoolGet() (*datalayer.EndpointPool, error)
 	ObjectiveGet(objectiveName string) *v1alpha2.InferenceObjective
 	PodList(predicate func(backendmetrics.PodMetrics) bool) []backendmetrics.PodMetrics
 }
@@ -291,10 +289,10 @@ func (d *Director) toSchedulerPodMetrics(pods []backendmetrics.PodMetrics) []sch
 // HandleResponseReceived is called when the response headers are received.
 func (d *Director) HandleResponseReceived(ctx context.Context, reqCtx *handlers.RequestContext) (*handlers.RequestContext, error) {
 	response := &Response{
-		RequestId: reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
-		Headers:   reqCtx.Response.Headers,
+		RequestId:   reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
+		Headers:     reqCtx.Response.Headers,
+		ReqMetadata: reqCtx.Request.Metadata,
 	}
-
 	// TODO: to extend fallback functionality, handle cases where target pod is unavailable
 	// https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1224
 	d.runResponseReceivedPlugins(ctx, reqCtx.SchedulingRequest, response, reqCtx.TargetPod)
diff --git a/pkg/epp/requestcontrol/types.go b/pkg/epp/requestcontrol/types.go
index c881ed7..7a67256 100644
--- a/pkg/epp/requestcontrol/types.go
+++ b/pkg/epp/requestcontrol/types.go
@@ -28,4 +28,8 @@ type Response struct {
 	IsStreaming bool
 	// EndOfStream when true indicates that this invocation contains the last chunk of the response
 	EndOfStream bool
+	// ReqMetadata is a map of metadata that can be passed from Envoy.
+	// It is populated with Envoy's dynamic metadata when ext_proc is processing ProcessingRequest_ResponseHeaders.
+	// Currently, this is only used by conformance test.
+	ReqMetadata map[string]any
 }
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
index d30bc13..2947785 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
@@ -67,15 +67,19 @@ const (
 )
 
 var DefaultConfig = Config{
-	DefaultBlockSize:       DefaultBlockSize,
+	AutoTune:               true,
+	BlockSize:              DefaultBlockSize,
 	MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
 	LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 }
 
 type Config struct {
+	// If set to true, the plugin will automatically adjust the configuration based on various
+	// metrics from the model servers.
+	AutoTune bool `json:"autoTune"`
 	// The input prompt is broken into sizes of BlockSize to calculate block hashes . Requests
 	// with length shorter than the block size will be ignored.
-	DefaultBlockSize int `json:"blockSize"`
+	BlockSize int `json:"blockSize"`
 	// MaxPrefixBlocksToMatch is the maximum number of prefix blocks to match. Input beyond this limit will
 	// be ignored.
 	MaxPrefixBlocksToMatch int `json:"maxPrefixBlocksToMatch"`
@@ -148,11 +152,7 @@ var (
 
 // PrefixCachePluginFactory defines the factory function for Prefix plugin.
 func PrefixCachePluginFactory(name string, rawParameters json.RawMessage, handle plugins.Handle) (plugins.Plugin, error) {
-	parameters := Config{
-		DefaultBlockSize:       DefaultBlockSize,
-		MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
-		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
-	}
+	parameters := DefaultConfig
 
 	if rawParameters != nil {
 		if err := json.Unmarshal(rawParameters, &parameters); err != nil {
@@ -167,40 +167,32 @@ func PrefixCachePluginFactory(name string, rawParameters json.RawMessage, handle
 
 // New initializes a new prefix Plugin and returns its pointer.
 func New(ctx context.Context, config Config) *Plugin {
-	capacity := config.LRUCapacityPerServer
-	if capacity <= 0 {
-		capacity = DefaultLRUCapacityPerServer
+	if config.LRUCapacityPerServer <= 0 {
+		config.LRUCapacityPerServer = DefaultLRUCapacityPerServer
 		log.FromContext(ctx).V(logutil.DEFAULT).Info(
 			"LRUCapacityPerServer is not positive, using default value",
 			"defaultCapacity", DefaultLRUCapacityPerServer,
 		)
 	}
 
-	blockSize := config.DefaultBlockSize
-	if blockSize <= 0 {
-		blockSize = DefaultBlockSize
-		log.FromContext(ctx).V(logutil.DEFAULT).Info("DefaultBlockSize is not positive, using default value",
+	if config.BlockSize <= 0 {
+		config.BlockSize = DefaultBlockSize
+		log.FromContext(ctx).V(logutil.DEFAULT).Info("BlockSize is not positive, using default value",
 			"default", DefaultBlockSize)
 	}
 
-	maxPrefixBlocks := config.MaxPrefixBlocksToMatch
-	if maxPrefixBlocks <= 0 {
-		maxPrefixBlocks = DefaultMaxPrefixBlocks
+	if config.MaxPrefixBlocksToMatch <= 0 {
+		config.MaxPrefixBlocksToMatch = DefaultMaxPrefixBlocks
 		log.FromContext(ctx).V(logutil.DEFAULT).Info("MaxPrefixBlocksToMatch is not positive, using default value",
 			"default", DefaultMaxPrefixBlocks)
 	}
 
-	validConfig := Config{
-		DefaultBlockSize:       blockSize,
-		MaxPrefixBlocksToMatch: maxPrefixBlocks,
-		LRUCapacityPerServer:   capacity,
-	}
-
+	log.FromContext(ctx).V(logutil.DEFAULT).Info("PrefixCachePlugin initialized", "config", config)
 	return &Plugin{
 		typedName:   plugins.TypedName{Type: PrefixCachePluginType, Name: PrefixCachePluginType},
-		config:      validConfig,
+		config:      config,
 		pluginState: plugins.NewPluginState(ctx),
-		indexer:     newIndexer(ctx, capacity),
+		indexer:     newIndexer(ctx, config.LRUCapacityPerServer),
 	}
 }
 
@@ -218,7 +210,7 @@ func (p *Plugin) WithName(name string) *Plugin {
 // Score returns the scoring result for the given list of pods based on context.
 func (p *Plugin) Score(ctx context.Context, cycleState *types.CycleState, request *types.LLMRequest, pods []types.Pod) map[types.Pod]float64 {
 	// pre score step, hashing prompt and find longest prefix match.
-	hashes := hashPrompt(ctx, request, getBlockSize(pods, p.config.DefaultBlockSize), p.config.MaxPrefixBlocksToMatch)
+	hashes := hashPrompt(ctx, request, getBlockSize(pods, p.config), p.config.MaxPrefixBlocksToMatch)
 	state := &SchedulingContextState{
 		PrefixHashes:       hashes,
 		PrefixCacheServers: p.matchLongestPrefix(ctx, hashes),
@@ -248,8 +240,12 @@ func (p *Plugin) Score(ctx context.Context, cycleState *types.CycleState, reques
 // PreRequest records in the plugin cache the result of the scheduling selection.
 func (p *Plugin) PreRequest(ctx context.Context, request *types.LLMRequest, schedulingResult *types.SchedulingResult) {
 	primaryProfileResult := schedulingResult.ProfileResults[schedulingResult.PrimaryProfileName]
-	targetPod := primaryProfileResult.TargetPods[0].GetPod() // get the first pod of the primary profile
-	gpuBlocks := primaryProfileResult.TargetPods[0].GetMetrics().CacheNumGPUBlocks
+	targetPod := primaryProfileResult.TargetPods[0] // get the first pod of the primary profile
+
+	gpuBlocks := p.config.LRUCapacityPerServer
+	if p.config.AutoTune && targetPod.GetMetrics().CacheNumGPUBlocks > 0 {
+		gpuBlocks = targetPod.GetMetrics().CacheNumGPUBlocks
+	}
 
 	state, err := plugins.ReadPluginStateKey[*SchedulingContextState](p.pluginState, request.RequestId, plugins.StateKey(p.TypedName().String()))
 	p.pluginState.Delete(request.RequestId) // delete the state explicitly after completing using it
@@ -265,16 +261,16 @@ func (p *Plugin) PreRequest(ctx context.Context, request *types.LLMRequest, sche
 	p.wg.Add(1)
 	go func() {
 		p.indexer.Add(state.PrefixHashes, Server{
-			ServerID(targetPod.NamespacedName),
+			ServerID(targetPod.GetPod().NamespacedName),
 			gpuBlocks,
 		})
 		p.wg.Done()
 	}()
 
 	total := len(state.PrefixHashes)
-	matchLen := state.PrefixCacheServers[ServerID(targetPod.NamespacedName)]
+	matchLen := state.PrefixCacheServers[ServerID(targetPod.GetPod().NamespacedName)]
 
-	blockSize := getBlockSize(primaryProfileResult.TargetPods, p.config.DefaultBlockSize)
+	blockSize := getBlockSize(primaryProfileResult.TargetPods, p.config)
 	metrics.RecordPrefixCacheMatch(matchLen*blockSize, total*blockSize)
 }
 
@@ -388,9 +384,14 @@ func getUserInputBytes(request *types.LLMRequest) ([]byte, error) {
 	return json.Marshal(request.Body.ChatCompletions.Messages)
 }
 
-func getBlockSize(pods []types.Pod, defaultBlockSize int) int {
+func getBlockSize(pods []types.Pod, config Config) int {
+	if !config.AutoTune {
+		return config.BlockSize
+	}
+
+	// Fallback to BlockSize if no metrics are available.
 	if len(pods) == 0 {
-		return defaultBlockSize
+		return config.BlockSize
 	}
 
 	// Since all PODs originate from the same inference pool, they are considered to have identical configurations.
@@ -401,5 +402,5 @@ func getBlockSize(pods []types.Pod, defaultBlockSize int) int {
 			return cacheBlockSize * averageCharactersPerToken
 		}
 	}
-	return defaultBlockSize
+	return config.BlockSize
 }
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/config.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/config.go
new file mode 100644
index 0000000..bbfd772
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/config.go
@@ -0,0 +1,191 @@
+/*
+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 requestcontrol contains helpers to decouple latency-predictor logic.
+package slo_aware_router
+
+import (
+	"os"
+	"strconv"
+	"strings"
+)
+
+var DefaultSamplingMean = func() float64 {
+	if value, exists := os.LookupEnv("SAMPLING_MEAN"); exists {
+		if parsedValue, err := strconv.ParseFloat(value, 64); err == nil && parsedValue > 0 {
+			return parsedValue
+		}
+	}
+	return 100.0 // default value
+}()
+
+var MaxSampledTokens = func() int {
+	if value, exists := os.LookupEnv("MAX_SAMPLED_TOKENS"); exists {
+		if parsedValue, err := strconv.Atoi(value); err == nil && parsedValue > 0 {
+			return parsedValue
+		}
+	}
+	return 20 // default value
+}()
+
+var SLOBufferFactor = func() float64 {
+	if value, exists := os.LookupEnv("SLO_BUFFER_FACTOR"); exists {
+		if parsedValue, err := strconv.ParseFloat(value, 64); err == nil {
+			return parsedValue
+		}
+	}
+	return 1.0 // default value
+}()
+
+var NegHeadroomTTFTWeight = func() float64 {
+	if value, exists := os.LookupEnv("NEG_HEADROOM_TTFT_WEIGHT"); exists {
+		if parsedValue, err := strconv.ParseFloat(value, 64); err == nil && parsedValue >= 0 {
+			return parsedValue
+		}
+	}
+	return 0.8 // default: TTFT dominates when violating SLOs
+}()
+
+var NegHeadroomTPOTWeight = func() float64 {
+	if value, exists := os.LookupEnv("NEG_HEADROOM_TPOT_WEIGHT"); exists {
+		if parsedValue, err := strconv.ParseFloat(value, 64); err == nil && parsedValue >= 0 {
+			return parsedValue
+		}
+	}
+	return 0.2 // default: TPOT less important in your tiny-output scenario
+}()
+
+var HeadroomTTFTWeight = func() float64 {
+	if value, exists := os.LookupEnv("HEADROOM_TTFT_WEIGHT"); exists {
+		if parsedValue, err := strconv.ParseFloat(value, 64); err == nil && parsedValue >= 0 {
+			return parsedValue
+		}
+	}
+	return 0.8 // default
+}()
+
+var HeadroomTPOTWeight = func() float64 {
+	if value, exists := os.LookupEnv("HEADROOM_TPOT_WEIGHT"); exists {
+		if parsedValue, err := strconv.ParseFloat(value, 64); err == nil && parsedValue >= 0 {
+			return parsedValue
+		}
+	}
+	return 0.2 // default
+}()
+
+var HeadroomSelectionStrategy = func() headroomStrategy {
+	if value, exists := os.LookupEnv("HEADROOM_SELECTION_STRATEGY"); exists {
+		switch strings.ToLower(value) {
+		case "least":
+			return headroomStrategyLeast
+		case "most":
+			return headroomStrategyMost
+		case "composite-least":
+			return headroomStrategyCompositeLeast
+		case "composite-most":
+			return headroomStrategyCompositeMost
+		case "composite-only":
+			return headroomStrategyCompositeOnly
+		}
+	}
+	return headroomStrategyLeast // default to least (better packing)
+}()
+
+// If using composite headroom, weights for each component. Not used by default
+var CompositeKVWeight = func() float64 {
+	if v, ok := os.LookupEnv("COMPOSITE_KV_WEIGHT"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 {
+			return f
+		}
+	}
+	return 1
+}()
+
+var CompositeQueueWeight = func() float64 {
+	if v, ok := os.LookupEnv("COMPOSITE_QUEUE_WEIGHT"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 {
+			return f
+		}
+	}
+	return 1
+}()
+
+var CompositePrefixWeight = func() float64 {
+	if v, ok := os.LookupEnv("COMPOSITE_PREFIX_WEIGHT"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 {
+			return f
+		}
+	}
+	return 1
+}()
+
+// With probability ε, explore (ignore affinity gate); otherwise exploit.
+var EpsilonExploreSticky = func() float64 {
+	// Prefer new env; fall back to old for compatibility.
+	if v, ok := os.LookupEnv("STICKY_EPSILON"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
+			return f
+		}
+	}
+	return 0.01 // default 1% exploration
+}()
+
+var EpsilonExploreNeg = func() float64 {
+	// Prefer new env; fall back to old for compatibility.
+	if v, ok := os.LookupEnv("NEG_HEADROOM_EPSILON"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
+			return f
+		}
+	}
+	return 0.01 // default 1% exploration
+}()
+
+// τ for per-path affinity gate (aka "stickiness" threshold).
+var AffinityGateTau = func() float64 {
+	// Prefer new env; fall back to old for compatibility.
+	if v, ok := os.LookupEnv("AFFINITY_GATE_TAU"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
+			return f
+		}
+	}
+	return 0.80
+}()
+
+// Global τ for the overall candidate set (previously "overall stickiness").
+var AffinityGateTauGlobal = func() float64 {
+	// Prefer new env; fall back to old for compatibility.
+	if v, ok := os.LookupEnv("AFFINITY_GATE_TAU_GLOBAL"); ok {
+		if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
+			return f
+		}
+	}
+	return 0.99
+}()
+
+// Read once at init. Values: "linear" (default) or "max".
+var SelectionMode = func() podSelectionMode {
+	if v, ok := os.LookupEnv("POD_SELECTION_MODE"); ok {
+		switch strings.ToLower(v) {
+		case "max":
+			return podSelectionMax
+		case "linear":
+			fallthrough
+		default:
+			return podSelectionLinear
+		}
+	}
+	return podSelectionLinear
+}()
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/headers.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/headers.go
new file mode 100644
index 0000000..da0a86f
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/headers.go
@@ -0,0 +1,69 @@
+/*
+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 requestcontrol contains helpers to decouple latency-predictor logic.
+package slo_aware_router
+
+import (
+	"strconv"
+
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
+)
+
+// parseFloatHeader retrieves a header by name, parses it as a float64,
+// and returns the value or an error if the header is missing or invalid.
+func parseFloatHeader(request schedulingtypes.LLMRequest, headerName string) (float64, error) {
+	// 1. Get header value from the map
+	headerValue, ok := request.Headers[headerName]
+	if !ok {
+		return 0, nil // Header not found, return 0 and false
+	}
+
+	// 2. Parse the header value to a float64
+	parsedFloat, err := strconv.ParseFloat(headerValue, 64)
+	if err != nil {
+		return 0, errutil.Error{
+			Code: errutil.BadRequest,
+			Msg:  headerName + " must be a float",
+		}
+	}
+
+	// 3. Return the successfully parsed value
+	return parsedFloat, nil
+}
+
+// parseFloatHeader retrieves a header by name, parses it as a bool,
+// and returns the value or an error if the header is missing or invalid.
+func parseBoolHeader(request schedulingtypes.LLMRequest, headerName string) (bool, error) {
+	// 1. Get header value from the map
+	headerValue, ok := request.Headers[headerName]
+	if !ok {
+		return false, nil // Header not found, return 0 and false
+	}
+
+	// 2. Parse the header value to a bool
+	parsedBool, err := strconv.ParseBool(headerValue)
+	if err != nil {
+		return false, errutil.Error{
+			Code: errutil.BadRequest,
+			Msg:  headerName + " must be a bool",
+		}
+	}
+
+	// 3. Return the successfully parsed value
+	return parsedBool, nil
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/helpers.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/helpers.go
new file mode 100644
index 0000000..3b58206
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/helpers.go
@@ -0,0 +1,145 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"math"
+	"math/rand"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+func (s *SLOAwareRouter) selectFromCompositeScores(ctx context.Context, allPreds []podPredictionResult, r *rand.Rand, strategy headroomStrategy) schedulingtypes.Pod {
+	total := 0
+	choices := s.buildCompositeChoices(
+		ctx, allPreds, CompositeKVWeight, CompositeQueueWeight, CompositePrefixWeight, &total,
+	)
+	if strategy == headroomStrategyCompositeLeast {
+		// Invert weights for "least" strategy
+		for i := range choices {
+			choices[i].weight = minWeight + wMax - choices[i].weight
+		}
+	}
+	selectedPod := s.performWeightedRandomSelection(choices, total, allPreds, r)
+	return selectedPod
+}
+func (s *SLOAwareRouter) performWeightedRandomSelection(weightedChoices []choice, total int, candidates []podPredictionResult, r *rand.Rand) schedulingtypes.Pod {
+	if total == 0 {
+		return nil
+	}
+	logger := log.FromContext(context.Background())
+	// Check if MAX_SCORE_SELECTION env variable is set
+	if SelectionMode == podSelectionMax {
+
+		logger.V(logutil.DEBUG).Info("Pod selection mode: MAX - selecting pod with highest weight")
+		maxWeight := 0
+		var selectedPod schedulingtypes.Pod
+		for _, c := range weightedChoices {
+			if c.weight > maxWeight {
+				maxWeight = c.weight
+				selectedPod = c.podName
+			}
+		}
+		if selectedPod != nil {
+			return selectedPod
+		}
+		// Fallback to first pod if no selection made
+		return candidates[0].Pod
+	}
+
+	// Original weighted random selection logic
+	logger.V(logutil.DEBUG).Info("Pod selection mode: LINEAR - performing weighted random selection")
+	idx := r.Intn(total)
+	var selectedPod schedulingtypes.Pod
+
+	for _, c := range weightedChoices {
+		if idx < c.weight {
+			selectedPod = c.podName
+			break
+		}
+		idx -= c.weight
+	}
+
+	// If no pod was selected (shouldn't happen), fallback to first pod
+	if selectedPod == nil {
+		selectedPod = candidates[0].Pod
+	}
+
+	return selectedPod
+}
+func (s *SLOAwareRouter) buildCompositeChoices(
+	ctx context.Context,
+	candidates []podPredictionResult,
+	wkv, wq, wpref float64,
+	total *int,
+) []choice {
+
+	// Normalize weights
+	sumw := wkv + wq + wpref
+	if sumw <= 0 {
+		wkv, wq, wpref = 1, 0, 0
+	} else {
+		wkv /= sumw
+		wq /= sumw
+		wpref /= sumw
+	}
+
+	// Precompute queue stats
+	minQ, maxQ := math.MaxInt32, -1
+	queueCounts := make(map[string]int, len(candidates))
+	for _, p := range candidates {
+		q := p.Pod.GetMetrics().WaitingQueueSize
+		queueCounts[p.Pod.GetPod().String()] = q
+		if q < minQ {
+			minQ = q
+		}
+		if q > maxQ {
+			maxQ = q
+		}
+	}
+	den := float64(maxQ - minQ)
+
+	choices := make([]choice, 0, len(candidates))
+	for _, p := range candidates {
+		q := queueCounts[p.Pod.GetPod().String()]
+		relQueue := 1.0
+		if den > 0 {
+			relQueue = (float64(maxQ-q) / den)
+		}
+
+		kvUsage := p.Pod.GetMetrics().KVCacheUsagePercent
+		kvFree := (1.0 - kvUsage)
+		prefix := (p.PrefixCacheScore)
+
+		composite := wkv*kvFree + wq*relQueue + wpref*prefix
+		w := int(math.Round(float64(minWeight) + (float64(wMax-minWeight) * composite)))
+		*total += w
+		choices = append(choices, choice{podName: p.Pod, weight: w})
+
+		log.FromContext(ctx).V(logutil.TRACE).Info("Composite (neg/pos) score",
+			"pod", p.Pod.GetPod().String(),
+			"kvUsage", kvUsage, "kvFree", kvFree,
+			"queue", q, "relQueue", relQueue,
+			"prefix", prefix,
+			"wkv", wkv, "wq", wq, "wprefix", wpref,
+			"composite", composite, "weight", w)
+	}
+	return choices
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/latencypredictor_helper.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/latencypredictor_helper.go
new file mode 100644
index 0000000..7482de9
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/latencypredictor_helper.go
@@ -0,0 +1,405 @@
+/*
+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 requestcontrol contains helpers to decouple latency-predictor logic.
+package slo_aware_router
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
+
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
+	latencypredictor "sigs.k8s.io/gateway-api-inference-extension/sidecars/latencypredictorasync"
+)
+
+// refreshLastSeenMetrics updates sloCtx.LastSeenMetrics from the latest scheduling result.
+func refreshLastSeenMetrics(ctx context.Context, sloCtx *sloRequestContext) {
+	if sr := sloCtx.schedulingResult; sr != nil {
+		if pr := sr.ProfileResults[sr.PrimaryProfileName]; pr != nil && pr.TargetPods != nil {
+			for profileName, profileResult := range sr.ProfileResults {
+				if profileResult != nil && profileResult.TargetPods != nil && len(profileResult.TargetPods) > 0 {
+					sloCtx.lastSeenMetrics[profileName] = profileResult.TargetPods[0].GetMetrics().Clone()
+				}
+			}
+		}
+	} else {
+		log.FromContext(ctx).V(logutil.DEBUG).Info("No scheduling result found, skipping metrics refresh")
+	}
+}
+
+// GetMetricsForPrediction retrieves the latest metrics for prediction from sloCtx.LastSeenMetrics.
+func getLatestMetricsForProfile(sloCtx *sloRequestContext) (*backendmetrics.MetricsState, error) {
+	if len(sloCtx.lastSeenMetrics) == 0 {
+		return nil, errors.New("no last seen metrics available for prediction")
+	}
+
+	primaryProfileName := sloCtx.schedulingResult.PrimaryProfileName
+	if metrics, exists := sloCtx.lastSeenMetrics[primaryProfileName]; exists {
+		return metrics, nil
+	}
+
+	return nil, fmt.Errorf("no metrics found for primary profile %s", primaryProfileName)
+}
+
+// ProcessHeader refreshes metrics, applies TTFT prediction, updates sloCtx.PredictedTTFT and timestamp.
+func processHeaderForLatencyPrediction(
+	ctx context.Context,
+	predictor latencypredictor.PredictorInterface,
+	sloCtx *sloRequestContext,
+) error {
+	logger := log.FromContext(ctx)
+
+	// just for debugging, print the req context scheduling result cycle state
+	// print the raw scores in scheduling result
+
+	// Build prediction request
+	m, err := getLatestMetricsForProfile(sloCtx)
+	if err != nil {
+		logger.V(logutil.DEBUG).Info("Skipping prediction due to missing metrics", "error", err)
+		return err
+	}
+
+	targetPod := sloCtx.targetPod
+	prefix_cache_score := sloCtx.prefixCacheScoresForPods[targetPod.String()]
+
+	in := latencypredictor.PredictionRequest{
+		KVCachePercentage:  m.KVCacheUsagePercent,
+		InputTokenLength:   len(strings.Fields(sloCtx.schedulingRequest.Body.Completions.Prompt)),
+		NumRequestWaiting:  m.WaitingQueueSize,
+		NumRequestRunning:  m.RunningQueueSize,
+		NumTokensGenerated: 0,
+		PrefixCacheScore:   prefix_cache_score,
+	}
+
+	// Predict TTFT
+	start := time.Now()
+	p, err := predictor.Predict(ctx, in)
+	dur := time.Since(start)
+	switch {
+	case err != nil:
+		logger.V(logutil.DEBUG).Error(err, "header TTFT predict failed", "duration_ms", dur.Milliseconds())
+		sloCtx.predictedTTFT = 0
+	case p == nil:
+		logger.V(logutil.DEBUG).Info("header TTFT predict nil", "duration_ms", dur.Milliseconds())
+		sloCtx.predictedTTFT = 0
+	default:
+		logger.V(logutil.DEBUG).Info("header TTFT succeeded", "value_ms", p.TTFT, "duration_ms", dur.Milliseconds())
+		metrics.RecordRequestTTFTPredictionDuration(ctx, sloCtx.schedulingRequest.TargetModel, sloCtx.incomingModelName, dur.Seconds())
+
+		sloCtx.predictedTTFT = p.TTFT
+	}
+
+	// Advance timestamp for first token reference
+	sloCtx.lastTokenTimestamp = time.Now()
+	refreshLastSeenMetrics(ctx, sloCtx)
+	return err
+}
+
+// ProcessFirstToken records actual TTFT, trains, predicts first TPOT, updates sloCtx, and advances timestamp.
+func processFirstTokenForLatencyPrediction(
+	ctx context.Context,
+	predictor latencypredictor.PredictorInterface,
+	sloCtx *sloRequestContext,
+	now time.Time,
+) {
+	logger := log.FromContext(ctx)
+
+	initializeSampler(ctx, sloCtx)
+
+	// Actual TTFT
+	sloCtx.ttft = float64(now.Sub(sloCtx.requestReceivedTimestamp).Milliseconds())
+	sloCtx.generatedTokenCount = 1
+	m, err := getLatestMetricsForProfile(sloCtx)
+	if err != nil {
+		logger.V(logutil.DEBUG).Info("Skipping prediction due to missing metrics", "error", err)
+		return
+	}
+	targetPod := sloCtx.targetPod
+	prefixCacheScore := sloCtx.prefixCacheScoresForPods[targetPod.String()]
+
+	recordTTFTTrainingData(ctx, predictor, sloCtx, m, now, prefixCacheScore)
+
+	predictFirstTPOT(ctx, predictor, sloCtx)
+
+	// Advance timestamp
+	sloCtx.lastTokenTimestamp = now
+	// Refresh metrics
+	refreshLastSeenMetrics(ctx, sloCtx)
+}
+
+func initializeSampler(ctx context.Context, sloCtx *sloRequestContext) {
+	if sloCtx.tokenSampler == nil {
+		logger := log.FromContext(ctx)
+		requestID := sloCtx.schedulingRequest.Headers[requtil.RequestIdHeaderKey]
+		sloCtx.tokenSampler = newTokenSampler(requestID, DefaultSamplingMean, MaxSampledTokens)
+		logger.V(logutil.DEBUG).Info("Initialized token sampler for first token", "request_id", requestID, "next_prediction_token", sloCtx.tokenSampler.getNextSampleToken())
+	}
+}
+
+func recordTTFTTrainingData(
+	ctx context.Context,
+	predictor latencypredictor.PredictorInterface,
+	sloCtx *sloRequestContext,
+	m *backendmetrics.MetricsState,
+	now time.Time,
+	prefixCacheScore float64,
+) {
+	logger := log.FromContext(ctx)
+	// Train TTFT
+	entry := latencypredictor.TrainingEntry{
+		KVCachePercentage:  m.KVCacheUsagePercent,
+		InputTokenLength:   len(strings.Fields(sloCtx.schedulingRequest.Body.Completions.Prompt)),
+		ActualTTFT:         sloCtx.ttft,
+		ActualTPOT:         0,
+		Timestamp:          now,
+		NumRequestWaiting:  m.WaitingQueueSize,
+		NumRequestRunning:  m.RunningQueueSize,
+		NumTokensGenerated: 0,
+		PrefixCacheScore:   prefixCacheScore,
+	}
+	if err := predictor.AddTrainingDataBulk([]latencypredictor.TrainingEntry{entry}); err != nil {
+		logger.V(logutil.DEBUG).Error(err, "record TTFT training failed")
+	}
+}
+
+func predictFirstTPOT(
+	ctx context.Context,
+	predictor latencypredictor.PredictorInterface,
+	sloCtx *sloRequestContext,
+) {
+	logger := log.FromContext(ctx)
+	m, err := getLatestMetricsForProfile(sloCtx)
+	if err != nil {
+		logger.V(logutil.DEBUG).Info("Skipping first TPOT prediction due to missing metrics",
+			"error", err)
+		return
+	}
+
+	// Predict first TPOT
+	in := latencypredictor.PredictionRequest{
+		KVCachePercentage:  m.KVCacheUsagePercent,
+		InputTokenLength:   len(strings.Fields(sloCtx.schedulingRequest.Body.Completions.Prompt)),
+		NumRequestWaiting:  m.WaitingQueueSize,
+		NumRequestRunning:  m.RunningQueueSize,
+		NumTokensGenerated: sloCtx.generatedTokenCount,
+		PrefixCacheScore:   0,
+	}
+	start := time.Now()
+	p, err := predictor.Predict(ctx, in)
+	dur := time.Since(start)
+	if err != nil || p == nil {
+		logger.V(logutil.DEBUG).Error(err, "first TPOT predict failed", "duration_ms", dur.Milliseconds())
+		sloCtx.predictedTPOTObservations = append(sloCtx.predictedTPOTObservations, 0)
+		sloCtx.avgPredictedTPOT = calculateRunningAverage(sloCtx.avgPredictedTPOT, 0, len(sloCtx.predictedTPOTObservations))
+	} else {
+		logger.V(logutil.DEBUG).Info("first TPOT succeeded", "value_ms", p.TPOT, "duration_ms", dur.Milliseconds())
+		sloCtx.predictedTPOTObservations = append(sloCtx.predictedTPOTObservations, p.TPOT)
+		sloCtx.avgPredictedTPOT = calculateRunningAverage(sloCtx.avgPredictedTPOT, p.TPOT, len(sloCtx.predictedTPOTObservations))
+	}
+	metrics.RecordRequestTPOTPredictionDuration(ctx, sloCtx.schedulingRequest.TargetModel, sloCtx.incomingModelName, dur.Seconds())
+}
+
+// ProcessToken records actual inter-token latency, trains, predicts sampled TPOT, updates sloCtx, and advances timestamp.
+func processTokenForLatencyPrediction(
+	ctx context.Context,
+	predictor latencypredictor.PredictorInterface,
+	sloCtx *sloRequestContext,
+	now time.Time,
+) {
+	logger := log.FromContext(ctx)
+
+	// Initialize sampler if not yet
+	if sloCtx.tokenSampler == nil {
+		requestID := sloCtx.schedulingRequest.Headers[requtil.RequestIdHeaderKey]
+		sloCtx.tokenSampler = newTokenSampler(requestID, DefaultSamplingMean, MaxSampledTokens)
+		logger.V(logutil.DEBUG).Info("Initialized token sampler for subsequent tokens", "request_id", requestID, "next_prediction_token", sloCtx.tokenSampler.getNextSampleToken())
+	}
+
+	// Inter-token latency
+	latencyMs := float64(now.Sub(sloCtx.lastTokenTimestamp).Milliseconds())
+	sloCtx.generatedTokenCount++
+
+	// log the inter-token latency for predicted samples
+	if sloCtx.generatedTokenCount == 2 || sloCtx.tokenSampler.shouldPredict(sloCtx.generatedTokenCount) { // tricky logic, since next sample token is always +1 from current token
+		sloCtx.tpotObservations = append(sloCtx.tpotObservations, latencyMs)
+		sloCtx.avgTPOT = calculateRunningAverage(sloCtx.avgTPOT, latencyMs, len(sloCtx.tpotObservations))
+	}
+
+	m, err := getLatestMetricsForProfile(sloCtx)
+	if err != nil {
+		logger.V(logutil.DEBUG).Info("Skipping first TPOT prediction due to missing metrics",
+			"error", err)
+		return
+	}
+	// Record actual TPOT
+	entry := latencypredictor.TrainingEntry{
+		KVCachePercentage:  m.KVCacheUsagePercent,
+		InputTokenLength:   len(strings.Fields(sloCtx.schedulingRequest.Body.Completions.Prompt)),
+		ActualTTFT:         0,
+		ActualTPOT:         latencyMs,
+		Timestamp:          now,
+		NumRequestWaiting:  m.WaitingQueueSize,
+		NumRequestRunning:  m.RunningQueueSize,
+		NumTokensGenerated: sloCtx.generatedTokenCount - 1,
+		PrefixCacheScore:   0, // TPOT does not use prefix cache score
+	}
+	if err := predictor.AddTrainingDataBulk([]latencypredictor.TrainingEntry{entry}); err != nil {
+		logger.V(logutil.DEBUG).Error(err, "record TPOT training failed")
+	}
+
+	// Sampled predict
+	if sloCtx.tokenSampler.shouldPredict(sloCtx.generatedTokenCount) {
+		in := latencypredictor.PredictionRequest{
+			KVCachePercentage:  m.KVCacheUsagePercent,
+			InputTokenLength:   len(strings.Fields(sloCtx.schedulingRequest.Body.Completions.Prompt)),
+			NumRequestWaiting:  m.WaitingQueueSize,
+			NumRequestRunning:  m.RunningQueueSize,
+			NumTokensGenerated: sloCtx.generatedTokenCount,
+			PrefixCacheScore:   0, // TPOT does not use prefix cache score
+		}
+		start := time.Now()
+		p, err := predictor.Predict(ctx, in)
+		dur := time.Since(start)
+		if err != nil || p == nil {
+			logger.V(logutil.DEBUG).Error(err, "TPOT predict failed", "duration_ms", dur.Milliseconds())
+			sloCtx.predictedTPOTObservations = append(sloCtx.predictedTPOTObservations, 0)
+			sloCtx.avgPredictedTPOT = calculateRunningAverage(sloCtx.avgPredictedTPOT, 0, len(sloCtx.predictedTPOTObservations))
+		} else {
+			logger.V(logutil.DEBUG).Info("TPOT predict succeeded", "value_ms", p.TPOT, "duration_ms", dur.Milliseconds())
+			sloCtx.predictedTPOTObservations = append(sloCtx.predictedTPOTObservations, p.TPOT)
+			sloCtx.avgPredictedTPOT = calculateRunningAverage(sloCtx.avgPredictedTPOT, p.TPOT, len(sloCtx.predictedTPOTObservations))
+		}
+		metrics.RecordRequestTPOTPredictionDuration(ctx, sloCtx.schedulingRequest.TargetModel, sloCtx.incomingModelName, dur.Seconds())
+
+		sloCtx.tokenSampler.recordPrediction(sloCtx.generatedTokenCount)
+	}
+
+	// Advance timestamp
+	sloCtx.lastTokenTimestamp = now
+	// Refresh metrics
+	refreshLastSeenMetrics(ctx, sloCtx)
+}
+
+// bulkPredictWithMetrics performs bulk predictions for multiple pods using their metrics states.
+// Returns predictions in the same order as the input slices.
+func bulkPredictWithMetrics(
+	ctx context.Context,
+	predictor latencypredictor.PredictorInterface,
+	metricsStates []*backendmetrics.MetricsState,
+	prompts []string,
+	generatedTokenCounts []int,
+	prefixCacheScores []float64,
+) ([]*latencypredictor.PredictionResponse, error) {
+	logger := log.FromContext(ctx)
+
+	// Validate input lengths
+	if len(metricsStates) != len(prompts) || len(prompts) != len(generatedTokenCounts) || len(generatedTokenCounts) != len(prefixCacheScores) {
+		return nil, fmt.Errorf("input slice lengths must match: metrics=%d, prompts=%d, tokenCounts=%d, prefixScores=%d",
+			len(metricsStates), len(prompts), len(generatedTokenCounts), len(prefixCacheScores))
+	}
+
+	if len(metricsStates) == 0 {
+		return []*latencypredictor.PredictionResponse{}, nil
+	}
+
+	// Validate that no metrics state is nil
+	for i, metricsState := range metricsStates {
+		if metricsState == nil {
+			return nil, fmt.Errorf("metrics state at index %d cannot be nil", i)
+		}
+	}
+
+	// Build bulk prediction requests
+	bulkRequests := make([]latencypredictor.PredictionRequest, len(metricsStates))
+	for i := range metricsStates {
+		bulkRequests[i] = latencypredictor.PredictionRequest{
+			KVCachePercentage:  metricsStates[i].KVCacheUsagePercent,
+			InputTokenLength:   len(strings.Fields(prompts[i])),
+			NumRequestWaiting:  metricsStates[i].WaitingQueueSize,
+			NumRequestRunning:  metricsStates[i].RunningQueueSize,
+			NumTokensGenerated: generatedTokenCounts[i],
+			PrefixCacheScore:   prefixCacheScores[i],
+		}
+	}
+
+	// Perform bulk prediction
+	start := time.Now()
+	bulkResponse, err := predictor.PredictBulkStrict(ctx, bulkRequests)
+	duration := time.Since(start)
+
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(err, "bulk prediction failed",
+			"duration_ms", duration.Milliseconds(),
+			"request_count", len(bulkRequests))
+		return nil, err
+	}
+
+	if bulkResponse == nil {
+		logger.V(logutil.DEBUG).Info("bulk prediction returned nil",
+			"duration_ms", duration.Milliseconds())
+		return nil, errors.New("bulk prediction returned nil result")
+	}
+
+	// Convert to pointer slice for consistency with single prediction
+	results := make([]*latencypredictor.PredictionResponse, len(bulkResponse.Predictions))
+	for i := range bulkResponse.Predictions {
+		results[i] = &bulkResponse.Predictions[i]
+	}
+
+	logger.V(logutil.DEBUG).Info("bulk prediction succeeded",
+		"duration_ms", duration.Milliseconds(),
+		"request_count", len(bulkRequests),
+		"successful_predictions", bulkResponse.SuccessfulPredictions,
+		"failed_predictions", bulkResponse.FailedPredictions,
+		"processing_time_ms", bulkResponse.ProcessingTimeMs)
+
+	// Log detailed results if at trace level
+	if logger.V(logutil.TRACE).Enabled() {
+		for i, result := range results {
+			logger.V(logutil.TRACE).Info("bulk prediction result",
+				"index", i,
+				"ttft_ms", result.TTFT,
+				"tpot_ms", result.TPOT,
+				"input_tokens", bulkRequests[i].InputTokenLength,
+				"generated_tokens", bulkRequests[i].NumTokensGenerated,
+				"kv_cache_percent", bulkRequests[i].KVCachePercentage,
+				"waiting_queue", bulkRequests[i].NumRequestWaiting,
+				"running_queue", bulkRequests[i].NumRequestRunning,
+				"prefix_cache_score", bulkRequests[i].PrefixCacheScore)
+		}
+	}
+
+	return results, nil
+}
+
+// calculateRunningAverage calculates the running average efficiently
+func calculateRunningAverage(currentAvg float64, newValue float64, count int) float64 {
+	if count == 0 {
+		return 0
+	}
+	if count == 1 {
+		return newValue
+	}
+	return currentAvg + (newValue-currentAvg)/float64(count)
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/prediction.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/prediction.go
new file mode 100644
index 0000000..7d41de9
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/prediction.go
@@ -0,0 +1,148 @@
+/*
+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 requestcontrol contains helpers to decouple latency-predictor logic.
+package slo_aware_router
+
+import (
+	"context"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	latencypredictor "sigs.k8s.io/gateway-api-inference-extension/sidecars/latencypredictorasync"
+)
+
+type podPredictionResult struct {
+	Pod              schedulingtypes.Pod
+	TTFT             float64
+	TPOT             float64
+	TTFTValid        bool
+	TPOTValid        bool
+	IsValid          bool
+	Error            error
+	Headroom         float64 // Headroom for the pod, if applicable
+	TTFTHeadroom     float64 // TTFT headroom for the pod
+	PrefixCacheScore float64 // Prefix cache score for the pod
+}
+
+// generatePredictions creates prediction results for all candidate pods
+func (s *SLOAwareRouter) generatePredictions(ctx context.Context, state *schedulingtypes.CycleState, request *schedulingtypes.LLMRequest, sloCtx *sloRequestContext, candidatePods []schedulingtypes.Pod) ([]podPredictionResult, error) {
+	logger := log.FromContext(ctx)
+	predictions := make([]podPredictionResult, 0, len(candidatePods))
+
+	// Prepare inputs for bulk prediction
+	metricsStates := make([]*backendmetrics.MetricsState, len(candidatePods))
+	prompts := make([]string, len(candidatePods))
+	generatedTokenCounts := make([]int, len(candidatePods))
+	prefixCacheScores := make([]float64, len(candidatePods))
+
+	for i, pod := range candidatePods {
+		logger.V(logutil.TRACE).Info("Candidate pod for scheduling", "pod", pod.GetPod().String(), "metrics", pod.GetMetrics().String())
+
+		// Get prefix cache score for the pod
+		prefixCacheScore := s.getPrefixCacheScoreForPod(ctx, state, pod)
+		sloCtx.prefixCacheScoresForPods[pod.GetPod().String()] = prefixCacheScore
+
+		logger.V(logutil.DEBUG).Info("Prefix cache score for pod", "pod", pod.GetPod().String(), "prefixCacheScore", prefixCacheScore)
+
+		metricsStates[i] = pod.GetMetrics()
+		prompts[i] = request.Body.Completions.Prompt
+		generatedTokenCounts[i] = 1
+		prefixCacheScores[i] = prefixCacheScore
+	}
+
+	// Bulk predict
+	bulkPredictions, err := bulkPredictWithMetrics(ctx, s.latencypredictor, metricsStates, prompts, generatedTokenCounts, prefixCacheScores)
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(err, "Bulk prediction failed")
+		return nil, err
+	}
+
+	// Process results
+	for i, pod := range candidatePods {
+		prediction := bulkPredictions[i]
+		predResult := podPredictionResult{Pod: pod}
+
+		predResult.PrefixCacheScore = prefixCacheScores[i]
+		predResult.TTFT = prediction.TTFT
+		predResult.TPOT = prediction.TPOT
+
+		podMinTPOTSLO := s.getPodMinTPOTSLO(pod)
+		predResult.TTFTValid, predResult.TPOTValid, predResult.IsValid, predResult.Headroom, predResult.TTFTHeadroom = s.validatePrediction(prediction, sloCtx, podMinTPOTSLO)
+
+		logger.V(logutil.DEBUG).Info("Prediction for scheduling",
+			"pod", pod.GetPod().String(),
+			"prefixCacheScore", predResult.PrefixCacheScore,
+			"TTFT", prediction.TTFT,
+			"TPOT", prediction.TPOT,
+			"buffer", SLOBufferFactor,
+			"podMinTPOTSLO", podMinTPOTSLO,
+			"ttftSLO", sloCtx.ttftSLO,
+			"requestTPOTSLO", sloCtx.avgTPOTSLO,
+			"tpotHeadroom", predResult.Headroom,
+			"ttftHeadroom", predResult.TTFTHeadroom,
+			"tpotValid", predResult.TPOTValid,
+			"ttftValid", predResult.TTFTValid,
+			"headroomStrategy", s.headroomStrategy)
+
+		predictions = append(predictions, predResult)
+	}
+
+	return predictions, nil
+}
+
+// updateRequestContextWithPredictions updates the request context with prediction data
+func (s *SLOAwareRouter) updateRequestContextWithPredictions(sloCtx *sloRequestContext, predictions []podPredictionResult) {
+	for _, pred := range predictions {
+		if pred.Error == nil {
+			podKey := pred.Pod.GetPod().String()
+			if sloCtx.predictedTTFTForScheduling == nil {
+				sloCtx.predictedTTFTForScheduling = make(map[string]float64)
+			}
+			if sloCtx.predictedTPOTForScheduling == nil {
+				sloCtx.predictedTPOTForScheduling = make(map[string]float64)
+			}
+			sloCtx.predictedTTFTForScheduling[podKey] = pred.TTFT
+			sloCtx.predictedTPOTForScheduling[podKey] = pred.TPOT
+		}
+	}
+}
+
+func (s *SLOAwareRouter) validatePrediction(
+	pred *latencypredictor.PredictionResponse,
+	sloCtx *sloRequestContext,
+	podMinTPOTSLO float64,
+) (ttftOk, tpotOk, isValid bool, headroom float64, ttftHeadroom float64) {
+
+	bufferedTPOT := sloCtx.avgTPOTSLO * SLOBufferFactor
+	// a podMinTPOTSLO of 0 means no either no requests, or no TPOT SLOs specified on running requests
+	if podMinTPOTSLO > 0 {
+		if podMinTPOTSLO < sloCtx.avgTPOTSLO {
+			log.FromContext(context.Background()).V(logutil.DEBUG).Info("Pod min TPOT SLO is less than the req SLO, adjusting", "podMinTPOTSLO", podMinTPOTSLO, "bufferedTPOT", sloCtx.avgTPOTSLO)
+		}
+		bufferedTPOT = min(bufferedTPOT, podMinTPOTSLO*SLOBufferFactor)
+	}
+
+	tpotOk = pred.TPOT < bufferedTPOT
+	ttftOk = pred.TTFT < sloCtx.ttftSLO
+
+	isValid = ttftOk && tpotOk
+	headroom = bufferedTPOT - pred.TPOT
+	ttftHeadroom = sloCtx.ttftSLO - pred.TTFT
+	return
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/requestcontrol_hooks.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/requestcontrol_hooks.go
new file mode 100644
index 0000000..d505b11
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/requestcontrol_hooks.go
@@ -0,0 +1,263 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package slo_aware_router
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/go-logr/logr"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"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/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
+)
+
+var _ requestcontrol.PreRequest = &SLOAwareRouter{}
+var _ requestcontrol.ResponseReceived = &SLOAwareRouter{}
+var _ requestcontrol.ResponseStreaming = &SLOAwareRouter{}
+var _ requestcontrol.ResponseComplete = &SLOAwareRouter{}
+
+type sloRequestContext struct {
+	schedulingRequest         schedulingtypes.LLMRequest
+	targetPod                 *backend.Pod
+	schedulingResult          *schedulingtypes.SchedulingResult
+	lastSeenMetrics           map[string]*backendmetrics.MetricsState
+	lastTokenTimestamp        time.Time
+	requestReceivedTimestamp  time.Time
+	generatedTokenCount       int
+	incomingModelName         string
+	ttft                      float64
+	predictedTTFT             float64
+	avgTPOT                   float64
+	avgPredictedTPOT          float64
+	tokenSampler              *tokenSampler
+	tpotObservations          []float64
+	predictedTPOTObservations []float64
+
+	prefixCacheScoresForPods map[string]float64
+
+	// ttftSLO is the target time to first token SLO for the request.
+	ttftSLO float64
+	// TPOTSLO is the target time per output token SLO for the request.
+	avgTPOTSLO float64
+
+	// predictorBasedScheduling indicates whether to use predictor based scheduling.
+	predictorBasedScheduling bool
+	// predictedTTFTForScheduling is the map of pod names to predicted TTFT values for scheduling.
+	predictedTTFTForScheduling map[string]float64
+	// predictedTPOTForScheduling is the map of pod names to predicted TPOT values for scheduling.
+	predictedTPOTForScheduling map[string]float64
+
+	// boolean set if request has valid pod based on predictions
+	hasValidPod bool
+}
+
+func newSLORequestContext(request *schedulingtypes.LLMRequest) *sloRequestContext {
+	return &sloRequestContext{
+		schedulingRequest:          *request,
+		lastSeenMetrics:            make(map[string]*backendmetrics.MetricsState),
+		prefixCacheScoresForPods:   make(map[string]float64),
+		predictedTTFTForScheduling: make(map[string]float64),
+		predictedTPOTForScheduling: make(map[string]float64),
+	}
+}
+
+func (s *SLOAwareRouter) getSLOContextForRequest(request *schedulingtypes.LLMRequest) (*sloRequestContext, error) {
+	id := request.Headers[requtil.RequestIdHeaderKey]
+	if ctx, exists := s.sloContextStore.Load(id); exists {
+		return ctx.(*sloRequestContext), nil
+	}
+	return nil, fmt.Errorf("SLO context not found for request ID: %s", id)
+}
+
+func (s *SLOAwareRouter) setSLOContextForRequest(request *schedulingtypes.LLMRequest, ctx *sloRequestContext) {
+	id := request.Headers[requtil.RequestIdHeaderKey]
+	s.sloContextStore.Store(id, ctx)
+}
+
+func (s *SLOAwareRouter) deleteSLOContextForRequest(request *schedulingtypes.LLMRequest) {
+	id := request.Headers[requtil.RequestIdHeaderKey]
+	s.sloContextStore.Delete(id)
+}
+
+// --- RequestControl Hooks ---
+
+func (t *SLOAwareRouter) PreRequest(ctx context.Context, request *schedulingtypes.LLMRequest, schedulingResult *schedulingtypes.SchedulingResult) {
+	logger := log.FromContext(ctx)
+
+	if schedulingResult == nil || len(schedulingResult.ProfileResults) == 0 {
+		logger.V(logutil.TRACE).Info("SLOAwareRouter: Skipping PreRequest because no scheduling result was provided.")
+		return
+	}
+
+	targetPod := schedulingResult.ProfileResults[schedulingResult.PrimaryProfileName].TargetPods[0].GetPod()
+	if !t.checkPredictor(logger, targetPod) {
+		return
+	}
+
+	podName := types.NamespacedName{
+		Name:      targetPod.NamespacedName.Name,
+		Namespace: targetPod.NamespacedName.Namespace,
+	}
+
+	logger.V(logutil.TRACE).Info("request ID for SLO tracking", "requestID", request.Headers[requtil.RequestIdHeaderKey], "podName", podName)
+	if request.Headers[requtil.RequestIdHeaderKey] == "" {
+		logger.V(logutil.DEBUG).Error(errors.New("missing request ID"), "SLOAwareRouter.PreRequest: Request is missing request ID header")
+	}
+
+	id := request.Headers[requtil.RequestIdHeaderKey]
+	podRequestList, ok := t.runningRequestLists[podName]
+	if !ok {
+		podRequestList = newRequestPriorityQueue()
+		t.runningRequestLists[podName] = podRequestList
+	}
+
+	sloCtx, err := t.getSLOContextForRequest(request)
+	if err != nil {
+		id := request.Headers[requtil.RequestIdHeaderKey]
+		logger.V(logutil.DEBUG).Error(err, "SLOAwareRouter.PreRequest: Failed to get SLO context for request", "requestID", id)
+		return
+	}
+
+	added := podRequestList.Add(id, sloCtx.avgTPOTSLO)
+	if !added {
+		logger.V(logutil.TRACE).Info("SLOAwareRouter: Item already exists in queue", "podName", podName, "requestID", id)
+	}
+
+	// Set up SLO request context
+	sloCtx.targetPod = targetPod
+	sloCtx.schedulingResult = schedulingResult
+	sloCtx.requestReceivedTimestamp = time.Now()
+	refreshLastSeenMetrics(ctx, sloCtx)
+	t.setSLOContextForRequest(request, sloCtx)
+}
+
+func (t *SLOAwareRouter) ResponseReceived(ctx context.Context, request *schedulingtypes.LLMRequest, response *requestcontrol.Response, targetPod *backend.Pod) {
+	logger := log.FromContext(ctx)
+	if !t.checkPredictor(logger, targetPod) {
+		return
+	}
+
+	id := request.Headers[requtil.RequestIdHeaderKey]
+
+	sloCtx, err := t.getSLOContextForRequest(request)
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(err, "SLOAwareRouter: Failed to get SLO context for request", "requestID", id)
+		return
+	}
+
+	if err := processHeaderForLatencyPrediction(ctx, t.latencypredictor, sloCtx); err != nil {
+		logger.V(logutil.DEBUG).Error(err, "ProcessHeader in latencypredictor failed")
+	}
+
+}
+
+func (t *SLOAwareRouter) ResponseStreaming(ctx context.Context, request *schedulingtypes.LLMRequest, response *requestcontrol.Response, pod *backend.Pod) {
+	logger := log.FromContext(ctx)
+	if !t.checkPredictor(logger, pod) || response.EndOfStream {
+		return
+	}
+
+	now := time.Now()
+	sloCtx, err := t.getSLOContextForRequest(request)
+	if err != nil {
+		id := request.Headers[requtil.RequestIdHeaderKey]
+		logger.V(logutil.TRACE).Error(err, "SLOAwareRouter.ResponseStreaming: Failed to get SLO context for request", "requestID", id)
+		return
+	}
+
+	if sloCtx.ttft == 0 {
+		processFirstTokenForLatencyPrediction(ctx, t.latencypredictor, sloCtx, now)
+	} else {
+		processTokenForLatencyPrediction(ctx, t.latencypredictor, sloCtx, now)
+	}
+
+}
+
+func (t *SLOAwareRouter) ResponseComplete(ctx context.Context, request *schedulingtypes.LLMRequest, response *requestcontrol.Response, pod *backend.Pod) {
+	logger := log.FromContext(ctx)
+	targetPod := pod
+	if !t.checkPredictor(logger, targetPod) {
+		return
+	}
+
+	sloCtx, err := t.getSLOContextForRequest(request)
+	if err != nil {
+		id := request.Headers[requtil.RequestIdHeaderKey]
+		logger.V(logutil.DEBUG).Error(err, "SLOAwareRouter.ResponseComplete: Failed to get SLO context for request", "requestID", id)
+		return
+	}
+
+	if sloCtx.ttft > 0 {
+		logger.V(logutil.TRACE).Info("Averages calculated", "avgActualTTFT", sloCtx.ttft, "avgPredictedTTFT", sloCtx.predictedTTFT)
+		metrics.RecordRequestTTFT(ctx, sloCtx.incomingModelName, request.TargetModel, sloCtx.ttft/1000)
+		metrics.RecordRequestPredictedTTFT(ctx, sloCtx.incomingModelName, request.TargetModel, sloCtx.predictedTTFT/1000)
+		if sloCtx.ttftSLO > 0 {
+			metrics.RecordRequestTTFTWithSLO(ctx, sloCtx.incomingModelName, request.TargetModel, sloCtx.ttft, sloCtx.ttftSLO)
+		}
+	}
+
+	if sloCtx.avgTPOT > 0 {
+		logger.V(logutil.TRACE).Info("Averages calculated", "avgActualTPOT", sloCtx.avgTPOT, "avgPredictedTPOT", sloCtx.avgPredictedTPOT)
+		metrics.RecordRequestTPOT(ctx, sloCtx.incomingModelName, request.TargetModel, sloCtx.avgTPOT/1000)
+		metrics.RecordRequestPredictedTPOT(ctx, sloCtx.incomingModelName, request.TargetModel, sloCtx.avgPredictedTPOT/1000)
+		if sloCtx.avgTPOTSLO > 0 {
+			metrics.RecordRequestTPOTWithSLO(ctx, sloCtx.incomingModelName, request.TargetModel, sloCtx.avgTPOT, sloCtx.avgTPOTSLO)
+		}
+	}
+
+	logger.V(logutil.TRACE).Info("SLO Aware Routing Mode", "PredictorBasedScheduling", sloCtx.predictorBasedScheduling)
+
+	podName := types.NamespacedName{
+		Name:      targetPod.NamespacedName.Name,
+		Namespace: targetPod.NamespacedName.Namespace,
+	}
+
+	id := request.Headers[requtil.RequestIdHeaderKey]
+	podRequestList, ok := t.runningRequestLists[podName]
+	if !ok {
+		err := fmt.Errorf("no running request list found for pod %s", podName.String())
+		logger.V(logutil.DEBUG).Error(err, "SLOAwareRouter: Failed to remove request from queue", "requestID", id)
+	}
+
+	_, removed := podRequestList.Remove(id)
+	if !removed {
+		logger.V(logutil.TRACE).Info("SLOAwareRouter: Item not found in queue", "podName", podName, "requestID", id)
+	}
+	t.deleteSLOContextForRequest(request)
+}
+
+func (t *SLOAwareRouter) checkPredictor(logger logr.Logger, targetPod *backend.Pod) bool {
+	if targetPod == nil {
+		logger.V(logutil.TRACE).Info("SLOAwareRouter: Skipping hook because no target pod was provided.")
+		return false
+	}
+	if t.latencypredictor == nil {
+		logger.V(logutil.TRACE).Info("SLOAwareRouter: Skipping hook because predictor missing")
+		return false
+	}
+	return true
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/running_request_queue.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/running_request_queue.go
new file mode 100644
index 0000000..37017fb
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/running_request_queue.go
@@ -0,0 +1,243 @@
+/*
+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 slo_aware_router
+
+import (
+	"container/heap"
+	"fmt"
+	"sort"
+	"strings"
+	"sync"
+)
+
+// request represents an element in the priority queue.
+// The index is needed by heap.Remove and is maintained by the heap.Interface methods.
+type request struct {
+	id    string  // Unique identifier
+	tpot  float64 // The priority value (lower is higher priority)
+	index int
+}
+
+// requestPriorityQueue implements a priority queue with item removal by ID.
+type requestPriorityQueue struct {
+	items  []*request
+	lookup map[string]*request
+	mutex  sync.RWMutex
+}
+
+// newRequestPriorityQueue initializes and returns a new PriorityQueue.
+func newRequestPriorityQueue() *requestPriorityQueue {
+	return &requestPriorityQueue{
+		lookup: make(map[string]*request),
+		items:  []*request{},
+	}
+}
+
+// Clone creates a deep copy of the priority queue.
+// The new queue is completely independent of the original.
+func (pq *requestPriorityQueue) Clone() *requestPriorityQueue {
+	pq.mutex.RLock()
+	defer pq.mutex.RUnlock()
+
+	// Initialize a new priority queue with pre-allocated capacity.
+	clonedPq := &requestPriorityQueue{
+		items:  make([]*request, len(pq.items)),
+		lookup: make(map[string]*request, len(pq.lookup)),
+	}
+
+	// Iterate through the original items to create deep copies.
+	for i, oldItem := range pq.items {
+		// Create a new Request struct, copying all values.
+		newItem := &request{
+			id:    oldItem.id,
+			tpot:  oldItem.tpot,
+			index: oldItem.index,
+		}
+
+		// Assign the new item to the cloned queue's items slice.
+		clonedPq.items[i] = newItem
+		// Update the lookup map in the cloned queue to point to the new item.
+		clonedPq.lookup[newItem.id] = newItem
+	}
+
+	return clonedPq
+}
+
+// Len is the number of items in the queue.
+func (pq *requestPriorityQueue) Len() int { return len(pq.items) }
+
+// Less reports whether the item with index i should sort before the item with index j.
+func (pq *requestPriorityQueue) Less(i, j int) bool {
+	return pq.items[i].tpot < pq.items[j].tpot
+}
+
+// Swap swaps the items with indexes i and j.
+func (pq *requestPriorityQueue) Swap(i, j int) {
+	pq.items[i], pq.items[j] = pq.items[j], pq.items[i]
+	pq.items[i].index = i
+	pq.items[j].index = j
+}
+
+// Push adds an item to the heap.
+func (pq *requestPriorityQueue) Push(x any) {
+	item := x.(*request)
+	item.index = len(pq.items)
+	pq.items = append(pq.items, item)
+}
+
+// Pop removes and returns the minimum item from the heap.
+func (pq *requestPriorityQueue) Pop() any {
+	n := len(pq.items)
+	item := pq.items[n-1]
+	pq.items[n-1] = nil // avoid memory leak
+	item.index = -1     // for safety
+	pq.items = pq.items[0 : n-1]
+	return item
+}
+
+// Add adds a new item to the queue.
+// Returns true if the item was added, false if an item with the same ID already exists.
+func (pq *requestPriorityQueue) Add(id string, tpot float64) bool {
+	pq.mutex.Lock()
+	defer pq.mutex.Unlock()
+
+	// Validate input
+	if id == "" {
+		return false
+	}
+	if tpot < 0 {
+		return false
+	}
+
+	// If item already exists, do not add
+	if _, exists := pq.lookup[id]; exists {
+		return false
+	}
+
+	item := &request{
+		id:   id,
+		tpot: tpot,
+	}
+	pq.lookup[id] = item
+	heap.Push(pq, item)
+	return true
+}
+
+// Update modifies the TPOT value of an existing item in the queue.
+// If the item doesn't exist, this method does nothing.
+func (pq *requestPriorityQueue) Update(id string, tpot float64) bool {
+	pq.mutex.Lock()
+	defer pq.mutex.Unlock()
+
+	// Validate input
+	if tpot < 0 {
+		return false
+	}
+
+	item, exists := pq.lookup[id]
+	if !exists {
+		return false
+	}
+
+	item.tpot = tpot
+	heap.Fix(pq, item.index)
+	return true
+}
+
+// Remove removes an item from the queue by its ID.
+func (pq *requestPriorityQueue) Remove(id string) (*request, bool) {
+	pq.mutex.Lock()
+	defer pq.mutex.Unlock()
+
+	item, ok := pq.lookup[id]
+	if !ok {
+		return nil, false
+	}
+	removed := heap.Remove(pq, item.index).(*request)
+	delete(pq.lookup, id)
+	return removed, true
+}
+
+// Peek returns the item with the lowest value without removing it.
+func (pq *requestPriorityQueue) Peek() *request {
+	pq.mutex.RLock()
+	defer pq.mutex.RUnlock()
+
+	if len(pq.items) == 0 {
+		return nil
+	}
+	return pq.items[0]
+}
+
+// GetSize returns the current number of items in the queue.
+func (pq *requestPriorityQueue) GetSize() int {
+	pq.mutex.RLock()
+	defer pq.mutex.RUnlock()
+	return len(pq.items)
+}
+
+// Contains checks if an item with the given ID exists in the queue.
+func (pq *requestPriorityQueue) Contains(id string) bool {
+	pq.mutex.RLock()
+	defer pq.mutex.RUnlock()
+	_, exists := pq.lookup[id]
+	return exists
+}
+
+// ToSlice returns a copy of all items in the queue, sorted by ID for stable comparison.
+// This is primarily intended for testing and validation.
+func (pq *requestPriorityQueue) ToSlice() []*request {
+	pq.mutex.RLock()
+	defer pq.mutex.RUnlock()
+
+	// Create a copy to avoid returning a reference to the internal slice.
+	itemsCopy := make([]*request, len(pq.items))
+	copy(itemsCopy, pq.items)
+
+	// Sort by ID to have a deterministic order for comparison in tests.
+	sort.Slice(itemsCopy, func(i, j int) bool {
+		return itemsCopy[i].id < itemsCopy[j].id
+	})
+
+	return itemsCopy
+}
+
+// String returns a string representation of the queue for debugging.
+func (pq *requestPriorityQueue) String() string {
+	pq.mutex.RLock()
+	defer pq.mutex.RUnlock()
+
+	if len(pq.items) == 0 {
+		return "RequestPriorityQueue: []"
+	}
+
+	var builder strings.Builder
+	builder.WriteString("RequestPriorityQueue: [")
+
+	for i, item := range pq.items {
+		if i > 0 {
+			builder.WriteString(", ")
+		}
+		builder.WriteString(item.id)
+		builder.WriteString("(")
+		builder.WriteString(fmt.Sprintf("%.2f", item.tpot))
+		builder.WriteString(")")
+	}
+
+	builder.WriteString("]")
+	return builder.String()
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/sampler.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/sampler.go
new file mode 100644
index 0000000..13d2543
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/sampler.go
@@ -0,0 +1,111 @@
+/*
+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 slo_aware_router
+
+import (
+	"hash/fnv"
+	"math"
+	"math/rand"
+	"time"
+)
+
+// tokenSampler handles Poisson-distributed sampling for predictions only
+// Training happens on every token regardless of sampling
+type tokenSampler struct {
+	rng             *rand.Rand
+	nextSampleToken int
+	samplingMean    float64
+	maxSamples      int
+	sampleCount     int
+}
+
+func newTokenSampler(requestID string, samplingMean float64, maxSamples int) *tokenSampler {
+	// Use request ID hash as seed for reproducibility
+	seed := int64(0)
+	if requestID != "" {
+		hash := fnv.New64a()
+		hash.Write([]byte(requestID))
+		seed = int64(hash.Sum64())
+	}
+	if seed == 0 {
+		seed = time.Now().UnixNano()
+	}
+
+	sampler := &tokenSampler{
+		rng:          rand.New(rand.NewSource(seed)),
+		samplingMean: samplingMean,
+		maxSamples:   maxSamples,
+	}
+
+	// Set first sample token (skip token 1 since that's TTFT)
+	sampler.nextSampleToken = 2 + sampler.poissonNext()
+
+	return sampler
+}
+
+// poissonNext generates the next interval using Poisson distribution
+func (ts *tokenSampler) poissonNext() int {
+	lambda := ts.samplingMean
+	if lambda <= 0 {
+		return 1
+	}
+
+	// For small lambda, use Knuth's algorithm
+	if lambda < 30 {
+		l := math.Exp(-lambda)
+		k := 0
+		p := 1.0
+
+		for p > l {
+			k++
+			p *= ts.rng.Float64()
+		}
+		return k - 1
+	}
+
+	// For larger lambda, use normal approximation
+	normal := ts.rng.NormFloat64()
+	interval := int(math.Round(lambda + math.Sqrt(lambda)*normal))
+	if interval < 1 {
+		return 1
+	}
+	return interval
+}
+
+// shouldPredict determines if we should make a prediction for the current token
+func (ts *tokenSampler) shouldPredict(currentToken int) bool {
+	return currentToken == ts.nextSampleToken && ts.sampleCount < ts.maxSamples
+}
+
+// recordPrediction records that a prediction was made and calculates the next sample token
+func (ts *tokenSampler) recordPrediction(currentToken int) {
+	if ts.sampleCount >= ts.maxSamples {
+		return
+	}
+
+	ts.sampleCount++
+
+	if ts.sampleCount < ts.maxSamples {
+		interval := ts.poissonNext()
+		ts.nextSampleToken = currentToken + interval
+	}
+}
+
+// getNextSampleToken returns the next token to predict for
+func (ts *tokenSampler) getNextSampleToken() int {
+	return ts.nextSampleToken
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer.go
new file mode 100644
index 0000000..8d05418
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer.go
@@ -0,0 +1,264 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+	"sync"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"k8s.io/apimachinery/pkg/types"
+	"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/framework/plugins/multi/prefix"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	latencypredictor "sigs.k8s.io/gateway-api-inference-extension/sidecars/latencypredictorasync"
+)
+
+type SLOAwareRouter struct {
+	tn                  plugins.TypedName
+	latencypredictor    latencypredictor.PredictorInterface
+	runningRequestLists map[types.NamespacedName]*requestPriorityQueue
+	sloContextStore     sync.Map // map[string]*SLORequestContext
+	headroomStrategy    headroomStrategy
+}
+
+var _ framework.Scorer = &SLOAwareRouter{}
+
+func NewSLOAwareRouter(latencypredictor latencypredictor.PredictorInterface, strategy headroomStrategy) *SLOAwareRouter {
+	return &SLOAwareRouter{
+		tn:                  plugins.TypedName{Type: SLOAwareRouterPluginType, Name: SLOAwareRouterPluginType},
+		latencypredictor:    latencypredictor,
+		runningRequestLists: make(map[types.NamespacedName]*requestPriorityQueue),
+		sloContextStore:     sync.Map{},
+		headroomStrategy:    strategy,
+	}
+}
+
+func (s *SLOAwareRouter) TypedName() plugins.TypedName {
+	return s.tn
+}
+
+func (s *SLOAwareRouter) WithName(name string) *SLOAwareRouter {
+	s.tn.Name = name
+	return s
+}
+
+func (s *SLOAwareRouter) epsilonGreedyAffinityGate(
+	ctx context.Context,
+	candidates []podPredictionResult,
+	r *rand.Rand,
+	label string, // e.g. "positive" or "negative"
+	prefixStickyThreshold float64,
+) ([]podPredictionResult, bool) {
+	logger := log.FromContext(ctx)
+	if prefixStickyThreshold <= 0 {
+		// Affinity gating disabled
+		logger.V(logutil.DEBUG).Info("Affinity gating disabled (threshold <= 0)", "path", label)
+		return candidates, false
+	}
+	eligible := make([]podPredictionResult, 0, len(candidates))
+	for _, p := range candidates {
+		if p.PrefixCacheScore >= prefixStickyThreshold {
+			eligible = append(eligible, p)
+		}
+	}
+
+	// No eligible sticky pods? Explore (no gating).
+	if len(eligible) == 0 {
+		return candidates, false
+	}
+
+	// ε-exploration branch
+	if r.Float64() < EpsilonExploreSticky {
+		logger.V(logutil.DEBUG).Info("ε-greedy: exploring (ignoring affinity gate)",
+			"path", label, "epsilon", EpsilonExploreSticky, "eligibleCount", len(eligible))
+		return candidates, false
+	}
+
+	logger.V(logutil.DEBUG).Info("ε-greedy: exploiting (apply affinity gate)",
+		"path", label, "threshold", prefixStickyThreshold, "eligibleCount", len(eligible), "total", len(candidates))
+	return eligible, true
+}
+
+// scoreWithoutPredictions provides fallback scoring based only on prefix cache scores
+// when latency predictions are unavailable
+func (s *SLOAwareRouter) scoreWithoutPredictions(
+	ctx context.Context,
+	state *schedulingtypes.CycleState,
+	pods []schedulingtypes.Pod,
+	r *rand.Rand,
+) map[schedulingtypes.Pod]float64 {
+	logger := log.FromContext(ctx)
+	logger.V(logutil.TRACE).Info("Using composite-only scoring without predictions")
+
+	scores := make(map[schedulingtypes.Pod]float64, len(pods))
+	for _, pod := range pods {
+		scores[pod] = 0
+	}
+
+	if len(pods) == 0 {
+		return scores
+	}
+
+	// Build prediction results with only prefix cache scores
+	podResults := make([]podPredictionResult, 0, len(pods))
+	for _, pod := range pods {
+		prefixScore := s.getPrefixCacheScoreForPod(ctx, state, pod)
+		podResults = append(podResults, podPredictionResult{
+			Pod:              pod,
+			PrefixCacheScore: prefixScore,
+			IsValid:          true, // All pods are valid when we don't check predictions
+		})
+	}
+
+	// Select based on composite scores (prefix cache + other non-prediction metrics)
+	selectedPod := s.selectFromCompositeScores(ctx, podResults, r, headroomStrategyCompositeOnly)
+
+	if selectedPod != nil {
+		scores[selectedPod] = 1
+		logger.V(logutil.TRACE).Info("Selected pod using composite-only scoring", "pod", selectedPod.GetPod().String())
+	}
+
+	return scores
+}
+
+func (s *SLOAwareRouter) Score(ctx context.Context, state *schedulingtypes.CycleState, request *schedulingtypes.LLMRequest, pods []schedulingtypes.Pod) map[schedulingtypes.Pod]float64 {
+	logger := log.FromContext(ctx)
+	if s.latencypredictor == nil {
+		logger.V(logutil.DEBUG).Info("SLOAwareRouter: no predictor configured, returning nil scores")
+		return nil
+	}
+
+	sloCtx := s.getOrMakeSLORequestContext(request)
+
+	s.parseSLOHeaders(ctx, request, sloCtx)
+
+	// Check if SLOs are provided
+	if !sloCtx.predictorBasedScheduling {
+		logger.V(logutil.DEBUG).Info("PredictorBasedScheduling turned off, skipping prediction-based filtering")
+		s.setSLOContextForRequest(request, sloCtx)
+		return nil
+	}
+
+	// Initialize scores map with all pods having score 0
+	scores := make(map[schedulingtypes.Pod]float64, len(pods))
+	for _, pod := range pods {
+		scores[pod] = 0
+	}
+
+	source := rand.NewSource(time.Now().UnixNano())
+	r := rand.New(source)
+	predictions, err := s.generatePredictions(ctx, state, request, sloCtx, pods)
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(err, "SLOAwareRouter: Error generating predictions, falling back to composite-only scoring")
+		// Fall back to composite-only scoring using prefix cache scores
+		s.setSLOContextForRequest(request, sloCtx)
+		return s.scoreWithoutPredictions(ctx, state, pods, r)
+	}
+	s.updateRequestContextWithPredictions(sloCtx, predictions)
+
+	allPreds := append([]podPredictionResult(nil), predictions...)
+	allPreds, sticky := s.epsilonGreedyAffinityGate(ctx, allPreds, r, "overall", AffinityGateTauGlobal)
+
+	// Check if all pods are invalid and all have running requests
+	allPodsInvalid := true
+	allPodsHaveRunningRequests := true
+
+	for _, pred := range allPreds {
+		if pred.IsValid {
+			allPodsInvalid = false
+		}
+
+		runningRequestCount := s.getPodRunningRequestCount(pred.Pod)
+		if runningRequestCount == 0 {
+			allPodsHaveRunningRequests = false
+		}
+	}
+
+	// Set HasValidPod to false if all pods are invalid and all have running requests
+	if allPodsInvalid && allPodsHaveRunningRequests && !sticky {
+		sloCtx.hasValidPod = false
+		logger.V(logutil.DEBUG).Info("All pods are invalid and have running requests, setting HasValidPod to false")
+	}
+
+	// 2) Tiered selection: positive headroom pods get 99% probability, negative get 1%
+	posHeadroomPods, negHeadroomPods := s.classifyPodsByHeadroom(allPreds)
+
+	logger.V(logutil.DEBUG).Info("Pod headroom distribution",
+		"positivePods", len(posHeadroomPods),
+		"negativePods", len(negHeadroomPods))
+
+	selectedPod := s.selectPodBasedOnStrategy(ctx, r, allPreds, posHeadroomPods, negHeadroomPods)
+
+	// Set score = 1 for selected pod, 0 for all others
+	if selectedPod != nil {
+		scores[selectedPod] = 1
+		logger.V(logutil.DEBUG).Info("Selected pod for scheduling", "pod", selectedPod.GetPod().String())
+	}
+
+	s.setSLOContextForRequest(request, sloCtx)
+
+	return scores
+}
+
+func (t *SLOAwareRouter) getOrMakeSLORequestContext(request *schedulingtypes.LLMRequest) *sloRequestContext {
+	sloCtx, err := t.getSLOContextForRequest(request)
+	if err != nil {
+		sloCtx = newSLORequestContext(request)
+	}
+	return sloCtx
+}
+
+func (s *SLOAwareRouter) getPrefixCacheScoreForPod(ctx context.Context, cycleState *schedulingtypes.CycleState, pod schedulingtypes.Pod) float64 {
+	log.FromContext(ctx).V(logutil.DEBUG).Info("Running getPrefixCacheScoreForPod, getting prefix cache score for pod", "pod", pod.GetPod().String())
+	plugintype := prefix.PrefixCachePluginType
+	pluginname := prefix.PrefixCachePluginType
+	cycleStateKey := (plugins.TypedName{Type: plugintype, Name: pluginname}).String()
+	stateData, err := cycleState.Read(plugins.StateKey(cycleStateKey))
+
+	log.FromContext(ctx).V(logutil.DEBUG).Info("Reading prefix cache state from cycle state", "stateKey", cycleStateKey)
+
+	if err != nil {
+		// The prefix cache plugin might not be enabled, which is a valid scenario.
+		log.FromContext(ctx).V(logutil.DEBUG).Info("prefix cache state not found in cycle state, returning prefix cache score of 0.0", "pod", pod.GetPod().String())
+		return 0.0
+	}
+
+	prefixCacheState, ok := stateData.(*prefix.SchedulingContextState)
+	if !ok {
+		// This should not happen if the plugin is configured correctly.
+		log.FromContext(ctx).Error(fmt.Errorf("unexpected state type: %T", stateData), "failed to read prefix cache state")
+		return 0.0
+	}
+
+	total := len(prefixCacheState.PrefixHashes)
+	if total == 0 {
+		// if the request has no prefixes, return 0.0
+		log.FromContext(ctx).V(logutil.DEBUG).Info("No prefixes found in request, returning prefix cache score of 0.0")
+		return 0.0
+	}
+
+	matchLen := prefixCacheState.PrefixCacheServers[prefix.ServerID(pod.GetPod().NamespacedName)]
+	log.FromContext(ctx).V(logutil.DEBUG).Info("Prefix cache score for pod", "pod", pod.GetPod().String(), "matchLen", matchLen, "totalPrefixes", total)
+	return float64(matchLen) / float64(total)
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer_helpers.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer_helpers.go
new file mode 100644
index 0000000..5b3fea8
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer_helpers.go
@@ -0,0 +1,102 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	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"
+)
+
+func (s *SLOAwareRouter) parseSLOHeaders(ctx context.Context, request *schedulingtypes.LLMRequest, sloCtx *sloRequestContext) {
+	logger := log.FromContext(ctx)
+	var err error
+
+	// Get Request SLOs from request header
+	sloCtx.ttftSLO, err = parseFloatHeader(*request, ttftSLOHeaderKey)
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(errutil.Error{Code: errutil.BadRequest, Msg: fmt.Sprintf("%v must be a float: %v", ttftSLOHeaderKey, err)}, "SLOAwareRouter: Error parsing TTFT SLO from header")
+	}
+
+	sloCtx.avgTPOTSLO, err = parseFloatHeader(*request, tpotSLOHeaderKey)
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(errutil.Error{Code: errutil.BadRequest, Msg: fmt.Sprintf("%v must be a float: %v", tpotSLOHeaderKey, err)}, "SLOAwareRouter: Error parsing TPOT SLO from header")
+	}
+	sloCtx.predictorBasedScheduling, err = parseBoolHeader(*request, "x-prediction-based-scheduling")
+	if err != nil {
+		logger.V(logutil.DEBUG).Error(errutil.Error{Code: errutil.BadRequest, Msg: fmt.Sprintf("x-prediction-based-scheduling must be a bool: %v", err)}, "SLOAwareRouter: Error parsing PredictorBasedScheduling from header")
+	}
+}
+
+func (s *SLOAwareRouter) classifyPodsByHeadroom(allPreds []podPredictionResult) (posHeadroomPods, negHeadroomPods []podPredictionResult) {
+	for _, p := range allPreds {
+		// A pod has positive headroom only if BOTH TTFT and TPOT have positive headroom
+		if p.Headroom > 0 && p.TTFTHeadroom > 0 {
+			posHeadroomPods = append(posHeadroomPods, p)
+		} else {
+			// A pod has negative headroom if EITHER TTFT or TPOT has negative/zero headroom
+			negHeadroomPods = append(negHeadroomPods, p)
+		}
+	}
+	return
+}
+
+func (s *SLOAwareRouter) selectPodBasedOnStrategy(
+	ctx context.Context,
+	r *rand.Rand,
+	allPreds, posHeadroomPods, negHeadroomPods []podPredictionResult,
+) schedulingtypes.Pod {
+	logger := log.FromContext(ctx)
+	var selectedPod schedulingtypes.Pod
+
+	switch {
+	case s.headroomStrategy == headroomStrategyCompositeOnly:
+		logger.V(logutil.DEBUG).Info("Selecting from composite scores only")
+		selectedPod = s.selectFromCompositeScores(ctx, allPreds, r, headroomStrategyCompositeOnly)
+	case len(posHeadroomPods) > 0 && len(negHeadroomPods) > 0:
+		// 99% chance to select from positive headroom pods, 1% from negative
+		if r.Float64() < EpsilonExploreNeg {
+			logger.V(logutil.DEBUG).Info("Selecting from negative headroom pods (1% chance)")
+			selectedPod = s.selectFromNegativeHeadroomPods(ctx, negHeadroomPods, r)
+		} else {
+			logger.V(logutil.DEBUG).Info("Selecting from positive headroom pods (99% chance)")
+			selectedPod = s.selectFromPositiveHeadroomPods(ctx, posHeadroomPods, r)
+		}
+	case len(posHeadroomPods) > 0:
+		// If only positive headroom pods exist, select from them
+		logger.V(logutil.DEBUG).Info("Only positive headroom pods available")
+		selectedPod = s.selectFromPositiveHeadroomPods(ctx, posHeadroomPods, r)
+	case len(negHeadroomPods) > 0:
+		// If only negative headroom pods exist, select from them
+		logger.V(logutil.DEBUG).Info("Only negative headroom pods available")
+		selectedPod = s.selectFromNegativeHeadroomPods(ctx, negHeadroomPods, r)
+	case len(allPreds) > 0:
+		// fallback - select randomly from valid pods
+		logger.V(logutil.DEBUG).Info("No headroom pods available, selecting randomly from valid pods")
+		selectedPod = allPreds[r.Intn(len(allPreds))].Pod
+	default:
+		// No valid pods - return nil (caller handles this)
+		logger.V(logutil.DEBUG).Info("No valid pods available")
+		return nil
+	}
+	return selectedPod
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/selection.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/selection.go
new file mode 100644
index 0000000..02a99cc
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/selection.go
@@ -0,0 +1,310 @@
+/*
+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 requestcontrol contains helpers to decouple latency-predictor logic.
+package slo_aware_router
+
+import (
+	"context"
+	"math"
+	"math/rand"
+
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+// selectFromPositiveHeadroomPods selects a pod from positive headroom pods using headroom strategy
+// Updated to incorporate TTFTHeadroom with a configurable blend vs TPOT headroom.
+func (s *SLOAwareRouter) selectFromPositiveHeadroomPods(ctx context.Context, posHeadroomPods []podPredictionResult, r *rand.Rand) schedulingtypes.Pod {
+
+	if len(posHeadroomPods) == 1 {
+		return posHeadroomPods[0].Pod
+	}
+
+	// Apply perfect stickiness (with exploration)
+	candidates, sticky := s.epsilonGreedyAffinityGate(ctx, posHeadroomPods, r, "positive", AffinityGateTau)
+
+	// If perfect stickiness collapsed us to a single pod, short-circuit
+	if sticky && len(candidates) == 1 {
+		return candidates[0].Pod
+	}
+	switch s.headroomStrategy {
+	case headroomStrategyCompositeMost:
+		return s.selectFromCompositeScores(ctx, candidates, r, headroomStrategyCompositeMost)
+	case headroomStrategyCompositeLeast:
+		return s.selectFromCompositeScores(ctx, candidates, r, headroomStrategyCompositeLeast)
+	}
+
+	// Find min/max for TPOT (Headroom) and TTFTHeadroom across positive pods to normalize to [0,1]
+	minTPOTH, maxTPOTH, minTTFTH, maxTTFTH := s.calculateHeadroomRanges(candidates)
+
+	// Calculate weights for weighted random selection
+	weightedChoices, total := s.calculateWeightedChoices(ctx, candidates, minTPOTH, maxTPOTH, minTTFTH, maxTTFTH)
+
+	return s.performWeightedRandomSelection(weightedChoices, total, candidates, r)
+}
+
+// selectFromNegativeHeadroomPods selects a pod from negative headroom pods using hierarchical TTFT/TPOT logic
+// Modified to strictly prefer pods with 0 running requests
+func (s *SLOAwareRouter) selectFromNegativeHeadroomPods(ctx context.Context, negHeadroomPods []podPredictionResult, r *rand.Rand) schedulingtypes.Pod {
+	logger := log.FromContext(ctx)
+
+	if len(negHeadroomPods) == 1 {
+		return negHeadroomPods[0].Pod
+	}
+
+	// First, separate pods by running request count
+	var zeroRunningRequestPods, nonZeroRunningRequestPods []podPredictionResult
+
+	for _, p := range negHeadroomPods {
+		runningRequestCount := s.getPodRunningRequestCount(p.Pod)
+		if runningRequestCount == 0 {
+			zeroRunningRequestPods = append(zeroRunningRequestPods, p)
+		} else {
+			nonZeroRunningRequestPods = append(nonZeroRunningRequestPods, p)
+		}
+	}
+
+	logger.V(logutil.DEBUG).Info("Negative headroom pods by running request count",
+		"zeroRunningRequests", len(zeroRunningRequestPods),
+		"nonZeroRunningRequests", len(nonZeroRunningRequestPods))
+
+	// If we have pods with 0 running requests, strictly prefer them
+	if len(zeroRunningRequestPods) > 0 {
+		logger.V(logutil.DEBUG).Info("Selecting from pods with zero running requests")
+		return s.selectFromNegativeHeadroomPodsInternal(ctx, zeroRunningRequestPods, r)
+	}
+
+	// Otherwise, fall back to pods with running requests
+	logger.V(logutil.DEBUG).Info("No pods with zero running requests, selecting from pods with running requests")
+	return s.selectFromNegativeHeadroomPodsInternal(ctx, nonZeroRunningRequestPods, r)
+}
+
+// selectFromNegativeHeadroomPodsInternal handles the actual selection logic for negative headroom pods
+func (s *SLOAwareRouter) selectFromNegativeHeadroomPodsInternal(ctx context.Context, negHeadroomPods []podPredictionResult, r *rand.Rand) schedulingtypes.Pod {
+	if len(negHeadroomPods) == 1 {
+		return negHeadroomPods[0].Pod
+	}
+
+	// Apply perfect stickiness (with exploration)
+	candidates, sticky := s.epsilonGreedyAffinityGate(ctx, negHeadroomPods, r, "negative", AffinityGateTau)
+
+	// If perfect stickiness collapsed us to a single pod, short-circuit
+	if sticky && len(candidates) == 1 {
+		return candidates[0].Pod
+	}
+
+	switch s.headroomStrategy {
+	case headroomStrategyCompositeMost:
+		return s.selectFromCompositeScores(ctx, candidates, r, headroomStrategyCompositeMost)
+	case headroomStrategyCompositeLeast:
+		return s.selectFromCompositeScores(ctx, candidates, r, headroomStrategyCompositeMost)
+	}
+
+	// Build weighted choices for selection
+	weightedChoices := make([]choice, 0, len(candidates))
+	total := 0
+
+	s.handleNegativeHeadroomPodsHierarchical(ctx, candidates, &weightedChoices, &total, minWeight)
+
+	// Perform weighted random selection
+	return s.performWeightedRandomSelection(weightedChoices, total, candidates, r)
+}
+
+// weightPodsByBlendedDeficit applies blended weighting using TTFT and TPOT deficits.
+// Lower blended deficit => higher weight.
+func (ps *SLOAwareRouter) weightPodsByBlendedDeficit(
+	ctx context.Context,
+	pods []podPredictionResult,
+	choices *[]choice,
+	total *int,
+	minWeight int,
+	alpha, beta float64, // weights for TTFT and TPOT deficits
+	category string,
+) {
+	logger := log.FromContext(ctx)
+	if len(pods) == 0 {
+		return
+	}
+
+	const Wrange = 80
+	const eps = 1e-9
+
+	// Compute raw deficits (only when headroom is negative)
+	type deficits struct {
+		pod     podPredictionResult
+		ttftDef float64
+		tpotDef float64
+	}
+	defs := make([]deficits, 0, len(pods))
+
+	minTTFT, maxTTFT := math.MaxFloat64, -math.MaxFloat64
+	minTPOT, maxTPOT := math.MaxFloat64, -math.MaxFloat64
+
+	for _, p := range pods {
+		ttftDef := 0.0
+		if p.TTFTHeadroom < 0 {
+			ttftDef = -p.TTFTHeadroom
+		}
+		tpotDef := 0.0
+		if p.Headroom < 0 {
+			tpotDef = -p.Headroom
+		}
+		defs = append(defs, deficits{pod: p, ttftDef: ttftDef, tpotDef: tpotDef})
+
+		if ttftDef < minTTFT {
+			minTTFT = ttftDef
+		}
+		if ttftDef > maxTTFT {
+			maxTTFT = ttftDef
+		}
+		if tpotDef < minTPOT {
+			minTPOT = tpotDef
+		}
+		if tpotDef > maxTPOT {
+			maxTPOT = tpotDef
+		}
+	}
+
+	ttftRange := maxTTFT - minTTFT
+	tpotRange := maxTPOT - minTPOT
+
+	// Normalize alpha/beta
+	if alpha+beta <= 0 {
+		alpha, beta = 1.0, 0.0
+	} else {
+		sum := alpha + beta
+		alpha /= sum
+		beta /= sum
+	}
+
+	logger.V(logutil.DEBUG).Info("Negative headroom blended deficits",
+		"category", category,
+		"minTTFTDef", minTTFT, "maxTTFTDef", maxTTFT,
+		"minTPOTDef", minTPOT, "maxTPOTDef", maxTPOT,
+		"alphaTTFT", alpha, "betaTPOT", beta, "podCount", len(pods))
+
+	for _, d := range defs {
+		// Normalize deficits to [0,1] within this bucket (0 = best / least violation)
+		nTTFT := 0.0
+		if ttftRange > eps {
+			nTTFT = (d.ttftDef - minTTFT) / (ttftRange + eps)
+		}
+		nTPOT := 0.0
+		if tpotRange > eps {
+			nTPOT = (d.tpotDef - minTPOT) / (tpotRange + eps)
+		}
+
+		// Blended "badness": higher = worse violation
+		blended := alpha*nTTFT + beta*nTPOT
+
+		// Convert to selection weight: lower badness -> higher weight
+		// Ensure a floor so no pod is completely excluded within the bucket.
+		w := int((1.0-blended)*float64(Wrange)) + minWeight + 1
+
+		*choices = append(*choices, choice{podName: d.pod.Pod, weight: w})
+		*total += w
+
+		logger.V(logutil.TRACE).Info("Negative bucket blended weighting",
+			"pod", d.pod.Pod.GetPod().String(),
+			"ttftDef", d.ttftDef, "tpotDef", d.tpotDef,
+			"normTTFT", nTTFT, "normTPOT", nTPOT,
+			"blendedBadness", blended, "weight", w)
+	}
+}
+
+func (s *SLOAwareRouter) handleNegativeHeadroomPodsHierarchical(
+	ctx context.Context,
+	negHeadroomPods []podPredictionResult,
+	choices *[]choice,
+	total *int,
+	minWeightForNegative int,
+) {
+	logger := log.FromContext(ctx)
+
+	// Categorize pods by their headroom status
+	var negTTFTNegTPOT, negTTFTNonNegTPOT, nonNegTTFTNegTPOT, nonNegTTFTNonNegTPOT []podPredictionResult
+
+	for _, p := range negHeadroomPods {
+		switch {
+		case p.TTFTHeadroom < 0 && p.Headroom < 0:
+			negTTFTNegTPOT = append(negTTFTNegTPOT, p)
+		case p.TTFTHeadroom < 0 && p.Headroom >= 0:
+			negTTFTNonNegTPOT = append(negTTFTNonNegTPOT, p)
+		case p.TTFTHeadroom >= 0 && p.Headroom < 0:
+			nonNegTTFTNegTPOT = append(nonNegTTFTNegTPOT, p)
+		default:
+			nonNegTTFTNonNegTPOT = append(nonNegTTFTNonNegTPOT, p)
+		}
+	}
+
+	logger.V(logutil.DEBUG).Info("Hierarchical negative headroom pod distribution",
+		"totalNegative", len(negHeadroomPods),
+		"negTTFT_negTPOT", len(negTTFTNegTPOT),
+		"negTTFT_nonNegTPOT", len(negTTFTNonNegTPOT),
+		"nonNegTTFT_negTPOT", len(nonNegTTFTNegTPOT),
+		"nonNegTTFT_nonNegTPOT", len(nonNegTTFTNonNegTPOT))
+
+	// Priority 1: both TTFT and TPOT negative -> blended deficits (both active)
+	if len(negTTFTNegTPOT) > 0 {
+		s.weightPodsByBlendedDeficit(ctx, negTTFTNegTPOT, choices, total, minWeightForNegative,
+			NegHeadroomTTFTWeight, NegHeadroomTPOTWeight, "both_negative")
+	}
+
+	// Priority 2: TTFT negative, TPOT non-negative -> blended still works (TPOT deficit=0)
+	if len(negTTFTNonNegTPOT) > 0 {
+		s.weightPodsByBlendedDeficit(ctx, negTTFTNonNegTPOT, choices, total, minWeightForNegative,
+			NegHeadroomTTFTWeight, NegHeadroomTPOTWeight, "ttft_negative")
+	}
+
+	// Priority 3: TTFT non-negative, TPOT negative -> blended (TTFT deficit=0)
+	if len(nonNegTTFTNegTPOT) > 0 {
+		s.weightPodsByBlendedDeficit(ctx, nonNegTTFTNegTPOT, choices, total, minWeightForNegative,
+			NegHeadroomTTFTWeight, NegHeadroomTPOTWeight, "tpot_negative")
+	}
+
+	// Priority 4: edge-case bucket -> minimal weight
+	for _, p := range nonNegTTFTNonNegTPOT {
+		*choices = append(*choices, choice{podName: p.Pod, weight: minWeightForNegative})
+		*total += minWeightForNegative
+	}
+}
+
+func (s *SLOAwareRouter) getPodMinTPOTSLO(pod schedulingtypes.Pod) float64 {
+	podName := types.NamespacedName{
+		Name:      pod.GetPod().NamespacedName.Name,
+		Namespace: pod.GetPod().NamespacedName.Namespace,
+	}
+	if runningReqs, ok := s.runningRequestLists[podName]; ok && runningReqs.GetSize() > 0 {
+		if topReq := runningReqs.Peek(); topReq != nil {
+			return topReq.tpot
+		}
+	}
+	return 0 // no running requests or no TPOT SLOs
+}
+
+func (s *SLOAwareRouter) getPodRunningRequestCount(pod schedulingtypes.Pod) int {
+	podName := types.NamespacedName{
+		Name:      pod.GetPod().NamespacedName.Name,
+		Namespace: pod.GetPod().NamespacedName.Namespace,
+	}
+	if runningReqs, ok := s.runningRequestLists[podName]; ok {
+		return runningReqs.GetSize()
+	}
+	return 0 // no running requests
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/selection_helpers.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/selection_helpers.go
new file mode 100644
index 0000000..cdf3d96
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/selection_helpers.go
@@ -0,0 +1,114 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"math"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+func (s *SLOAwareRouter) calculateHeadroomRanges(candidates []podPredictionResult) (minTPOTH, maxTPOTH, minTTFTH, maxTTFTH float64) {
+	minTPOTH, maxTPOTH = math.MaxFloat64, -math.MaxFloat64
+	minTTFTH, maxTTFTH = math.MaxFloat64, -math.MaxFloat64
+
+	for _, p := range candidates {
+		if p.Headroom < minTPOTH {
+			minTPOTH = p.Headroom
+		}
+		if p.Headroom > maxTPOTH {
+			maxTPOTH = p.Headroom
+		}
+		if p.TTFTHeadroom < minTTFTH {
+			minTTFTH = p.TTFTHeadroom
+		}
+		if p.TTFTHeadroom > maxTTFTH {
+			maxTTFTH = p.TTFTHeadroom
+		}
+	}
+	return
+}
+
+func (s *SLOAwareRouter) calculateWeightedChoices(
+	ctx context.Context,
+	candidates []podPredictionResult,
+	minTPOTH, maxTPOTH, minTTFTH, maxTTFTH float64,
+) ([]choice, int) {
+	logger := log.FromContext(ctx)
+	tpotRange := maxTPOTH - minTPOTH
+	ttftRange := maxTTFTH - minTTFTH
+
+	// Precompute blend weights (renormalize if user sets both to 0)
+	alpha := HeadroomTTFTWeight
+	beta := HeadroomTPOTWeight
+	if alpha+beta <= 0 {
+		alpha = 1.0
+		beta = 0.0
+	}
+	sum := alpha + beta
+	alpha /= sum
+	beta /= sum
+
+	logger.V(logutil.DEBUG).Info("Positive headroom normalization ranges",
+		"minTPOTHeadroom", minTPOTH, "maxTPOTHeadroom", maxTPOTH,
+		"minTTFTHeadroom", minTTFTH, "maxTTFTHeadroom", maxTTFTH,
+		"alphaTTFT", alpha, "betaTPOT", beta, "strategy", s.headroomStrategy)
+
+	weightedChoices := make([]choice, 0, len(candidates))
+	total := 0
+
+	for _, p := range candidates {
+		// Normalize to [0,1] within the cohort
+		nTPOTH := 0.5
+		if tpotRange > eps {
+			nTPOTH = (p.Headroom - minTPOTH) / (tpotRange + eps)
+		}
+		nTTFTH := 0.5
+		if ttftRange > eps {
+			nTTFTH = (p.TTFTHeadroom - minTTFTH) / (ttftRange + eps)
+		}
+
+		// Blend: larger combined -> "safer"; smaller -> "tighter packing"
+		combined := alpha*nTTFTH + beta*nTPOTH
+
+		// Map to integer weights
+		var w int
+		switch s.headroomStrategy {
+		case headroomStrategyLeast:
+			// prefer smaller combined headroom (pack closer to limits)
+			w = int((1.0-combined)*float64(wMax-minWeight)) + minWeight + 1
+		case headroomStrategyMost:
+			// prefer larger combined headroom (more conservative / spread)
+			w = int(combined*float64(wMax-minWeight)) + minWeight + 1
+		default:
+			// Fallback to least
+			w = int((1.0-combined)*float64(wMax-minWeight)) + minWeight + 1
+		}
+
+		weightedChoices = append(weightedChoices, choice{podName: p.Pod, weight: w})
+		total += w
+
+		logger.V(logutil.TRACE).Info("Positive headroom blended weight",
+			"pod", p.Pod.GetPod().String(),
+			"ttftHeadroom", p.TTFTHeadroom, "normTTFTHeadroom", nTTFTH,
+			"tpotHeadroom", p.Headroom, "normTPOTHeadroom", nTPOTH,
+			"combined", combined, "weight", w)
+	}
+	return weightedChoices, total
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/types.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/types.go
new file mode 100644
index 0000000..0384454
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/types.go
@@ -0,0 +1,57 @@
+/*
+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 requestcontrol contains helpers to decouple latency-predictor logic.
+package slo_aware_router
+
+import schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+
+type headroomStrategy string
+
+type choice struct {
+	podName schedulingtypes.Pod
+	weight  int
+}
+
+const (
+	// headroomStrategyLeast prioritizes pods with least positive headroom (better packing)
+	headroomStrategyLeast headroomStrategy = "least"
+	// headroomStrategyMost prioritizes pods with most positive headroom (more conservative)
+	headroomStrategyMost headroomStrategy = "most"
+
+	headroomStrategyCompositeLeast headroomStrategy = "composite-least"
+	headroomStrategyCompositeMost  headroomStrategy = "composite-most"
+	headroomStrategyCompositeOnly  headroomStrategy = "composite-only"
+
+	// TTFT header string
+	ttftSLOHeaderKey = "x-slo-ttft-ms"
+	// TPOT header string
+	tpotSLOHeaderKey = "x-slo-tpot-ms"
+)
+
+const (
+	SLOAwareRouterPluginType = "slo-aware-routing"
+	eps                      = 1e-9
+	wMax                     = 100
+	minWeight                = 1
+)
+
+type podSelectionMode string
+
+const (
+	podSelectionLinear podSelectionMode = "linear" // weighted-random (current behavior)
+	podSelectionMax    podSelectionMode = "max"    // pick argmax weight
+)
diff --git a/pkg/epp/scheduling/framework/plugins/profile/slo_aware_profile_handler.go b/pkg/epp/scheduling/framework/plugins/profile/slo_aware_profile_handler.go
new file mode 100644
index 0000000..c4bd638
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/profile/slo_aware_profile_handler.go
@@ -0,0 +1,152 @@
+/*
+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 profile
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+)
+
+const (
+	SLOAwareProfileHandlerType = "slo-aware-profile-handler"
+	DefaultProfileName         = "default"
+	PrefixProfileName          = "prefix"
+	SLOProfileName             = "slo"
+
+	// Boolean header string for whether to use predictor based scheduling
+	PreictionBasedSchedulingHeaderKey = "x-prediction-based-scheduling"
+)
+
+// compile-time type assertion
+var _ framework.ProfileHandler = &SLOAwareProfileHandler{}
+
+// SLOAwareProfileHandlerFactory defines the factory function for SLOAwareProfileHandler.
+func SLOAwareProfileHandlerFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewSLOAwareProfileHandler().WithName(name), nil
+}
+
+// NewSLOAwareProfileHandler initializes a new SLOAwareProfileHandler and returns its pointer.
+func NewSLOAwareProfileHandler() *SLOAwareProfileHandler {
+	return &SLOAwareProfileHandler{
+		typedName: plugins.TypedName{Type: SLOAwareProfileHandlerType, Name: SLOAwareProfileHandlerType},
+	}
+}
+
+// SLOAwareProfileHandler handles two profiles: the default profile and the SLO profile.
+// When the request has PredictorBasedScheduling=true, it uses the SLO profile result to select
+// the destination pod. Otherwise, it uses the default profile result.
+type SLOAwareProfileHandler struct {
+	typedName plugins.TypedName
+}
+
+// TypedName returns the type and name tuple of this plugin instance.
+func (h *SLOAwareProfileHandler) TypedName() plugins.TypedName {
+	return h.typedName
+}
+
+// WithName sets the name of the profile handler.
+func (h *SLOAwareProfileHandler) WithName(name string) *SLOAwareProfileHandler {
+	h.typedName.Name = name
+	return h
+}
+
+// Pick selects the SchedulingProfiles to run from the list of candidate profiles, while taking into consideration the request properties and the
+// previously executed cycles along with their results.
+func (h *SLOAwareProfileHandler) Pick(_ context.Context, _ *types.CycleState, request *types.LLMRequest, profiles map[string]*framework.SchedulerProfile,
+	profileResults map[string]*types.ProfileRunResult) map[string]*framework.SchedulerProfile {
+	if len(profiles) == len(profileResults) { // all profiles have been executed already in previous call
+		return map[string]*framework.SchedulerProfile{}
+	}
+
+	if _, executed := profileResults[PrefixProfileName]; !executed {
+		// if prefix profile was not executed yet, first let the scheduler run the decode profile
+		return map[string]*framework.SchedulerProfile{
+			PrefixProfileName: profiles[PrefixProfileName],
+		}
+	}
+	// otherwise, prefix was already executed.
+
+	// return all profiles except prefix.
+	profilesToRun := make(map[string]*framework.SchedulerProfile)
+	for name, profile := range profiles {
+		if name != PrefixProfileName {
+			profilesToRun[name] = profile
+		}
+	}
+	return profilesToRun
+}
+
+// ProcessResults handles the outcome of the profile runs after all profiles ran.
+// It may aggregate results, log test profile outputs, or apply custom logic. It specifies in the SchedulingResult the
+// key of the primary profile that should be used to get the request selected destination.
+// When a profile run fails, its result in the profileResults map is nil.
+func (h *SLOAwareProfileHandler) ProcessResults(ctx context.Context, _ *types.CycleState, request *types.LLMRequest, profileResults map[string]*types.ProfileRunResult) (*types.SchedulingResult, error) {
+
+	if len(profileResults) < 2 {
+		return nil, errors.New("SLOAwareProfileHandler requires at least two profiles to operate")
+	}
+
+	predictorBasedScheduling, err := parseBoolHeader(*request, PreictionBasedSchedulingHeaderKey)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing predictorBasedScheduling from header failed to choose scheduling profile: x-prediction-based-scheduling must be a bool: %v", err)
+	}
+
+	if predictorBasedScheduling { // TODO grab header directly from request.Headers instead of request field
+		if profileResults[SLOProfileName] == nil { // there was an error while running the SLO profile
+			return nil, fmt.Errorf("failed to run scheduler profile '%s'", SLOProfileName)
+		}
+		return &types.SchedulingResult{
+			ProfileResults:     profileResults,
+			PrimaryProfileName: SLOProfileName,
+		}, nil
+	}
+
+	if profileResults[DefaultProfileName] == nil { // there was an error while running the default profile
+		return nil, fmt.Errorf("failed to run scheduler profile '%s'", DefaultProfileName)
+	}
+
+	return &types.SchedulingResult{
+		ProfileResults:     profileResults,
+		PrimaryProfileName: DefaultProfileName,
+	}, nil
+}
+
+// parseFloatHeader retrieves a header by name, parses it as a bool,
+// and returns the value or an error if the header is missing or invalid.
+func parseBoolHeader(request types.LLMRequest, headerName string) (bool, error) {
+	// 1. Get header value from the map
+	headerValue, ok := request.Headers[headerName]
+	if !ok {
+		return false, nil // Header not found, return 0 and false
+	}
+
+	// 2. Parse the header value to a bool
+	parsedBool, err := strconv.ParseBool(headerValue)
+	if err != nil {
+		return false, fmt.Errorf("must be a bool: %v", headerName)
+	}
+
+	// 3. Return the successfully parsed value
+	return parsedBool, nil
+}
diff --git a/pkg/epp/server/controller_manager.go b/pkg/epp/server/controller_manager.go
index 47e4f12..c82b0bc 100644
--- a/pkg/epp/server/controller_manager.go
+++ b/pkg/epp/server/controller_manager.go
@@ -25,12 +25,12 @@ import (
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
@@ -45,7 +45,7 @@ func init() {
 }
 
 // defaultManagerOptions returns the default options used to create the manager.
-func defaultManagerOptions(gknn common.GKNN, metricsServerOptions metricsserver.Options) (ctrl.Options, error) {
+func defaultManagerOptions(disableK8sCrdReconcile bool, gknn common.GKNN, metricsServerOptions metricsserver.Options) (ctrl.Options, error) {
 	opt := ctrl.Options{
 		Scheme: scheme,
 		Cache: cache.Options{
@@ -55,38 +55,38 @@ func defaultManagerOptions(gknn common.GKNN, metricsServerOptions metricsserver.
 						gknn.Namespace: {},
 					},
 				},
-				&v1alpha2.InferenceObjective{}: {
-					Namespaces: map[string]cache.Config{
-						gknn.Namespace: {},
-					},
-				},
 			},
 		},
 		Metrics: metricsServerOptions,
 	}
-	switch gknn.Group {
-	case v1alpha2.GroupName:
-		opt.Cache.ByObject[&v1alpha2.InferencePool{}] = cache.ByObject{
-			Namespaces: map[string]cache.Config{gknn.Namespace: {FieldSelector: fields.SelectorFromSet(fields.Set{
-				"metadata.name": gknn.Name,
-			})}},
-		}
-	case v1.GroupName:
-		opt.Cache.ByObject[&v1.InferencePool{}] = cache.ByObject{
-			Namespaces: map[string]cache.Config{gknn.Namespace: {FieldSelector: fields.SelectorFromSet(fields.Set{
-				"metadata.name": gknn.Name,
-			})}},
+	if !disableK8sCrdReconcile {
+		opt.Cache.ByObject[&v1alpha2.InferenceObjective{}] = cache.ByObject{Namespaces: map[string]cache.Config{
+			gknn.Namespace: {},
+		}}
+		switch gknn.Group {
+		case v1alpha2.GroupName:
+			opt.Cache.ByObject[&v1alpha2.InferencePool{}] = cache.ByObject{
+				Namespaces: map[string]cache.Config{gknn.Namespace: {FieldSelector: fields.SelectorFromSet(fields.Set{
+					"metadata.name": gknn.Name,
+				})}},
+			}
+		case v1.GroupName:
+			opt.Cache.ByObject[&v1.InferencePool{}] = cache.ByObject{
+				Namespaces: map[string]cache.Config{gknn.Namespace: {FieldSelector: fields.SelectorFromSet(fields.Set{
+					"metadata.name": gknn.Name,
+				})}},
+			}
+		default:
+			return ctrl.Options{}, fmt.Errorf("unknown group: %s", gknn.Group)
 		}
-	default:
-		return ctrl.Options{}, fmt.Errorf("unknown group: %s", gknn.Group)
 	}
 
 	return opt, nil
 }
 
 // NewDefaultManager creates a new controller manager with default configuration.
-func NewDefaultManager(gknn common.GKNN, restConfig *rest.Config, metricsServerOptions metricsserver.Options, leaderElectionEnabled bool) (ctrl.Manager, error) {
-	opt, err := defaultManagerOptions(gknn, metricsServerOptions)
+func NewDefaultManager(disableK8sCrdReconcile bool, gknn common.GKNN, restConfig *rest.Config, metricsServerOptions metricsserver.Options, leaderElectionEnabled bool) (ctrl.Manager, error) {
+	opt, err := defaultManagerOptions(disableK8sCrdReconcile, gknn, metricsServerOptions)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create controller manager options: %v", err)
 	}
diff --git a/pkg/epp/server/runserver.go b/pkg/epp/server/runserver.go
index c303717..e43d849 100644
--- a/pkg/epp/server/runserver.go
+++ b/pkg/epp/server/runserver.go
@@ -30,9 +30,9 @@ import (
 	healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
-
 	"sigs.k8s.io/gateway-api-inference-extension/internal/runnable"
 	tlsutil "sigs.k8s.io/gateway-api-inference-extension/internal/tls"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
@@ -48,8 +48,8 @@ import (
 // ExtProcServerRunner provides methods to manage an external process server.
 type ExtProcServerRunner struct {
 	GrpcPort                         int
-	PoolNamespacedName               types.NamespacedName
-	PoolGKNN                         common.GKNN
+	GKNN                             common.GKNN
+	DisableK8sCrdReconcile           bool
 	Datastore                        datastore.Datastore
 	SecureServing                    bool
 	HealthChecking                   bool
@@ -91,7 +91,7 @@ const (
 // NewDefaultExtProcServerRunner creates a runner with default values.
 // Note: Dependencies like Datastore, Scheduler, SD need to be set separately.
 func NewDefaultExtProcServerRunner() *ExtProcServerRunner {
-	poolGKNN := common.GKNN{
+	gknn := common.GKNN{
 		NamespacedName: types.NamespacedName{Name: DefaultPoolName, Namespace: DefaultPoolNamespace},
 		GroupKind: schema.GroupKind{
 			Group: DefaultPoolGroup,
@@ -100,8 +100,8 @@ func NewDefaultExtProcServerRunner() *ExtProcServerRunner {
 	}
 	return &ExtProcServerRunner{
 		GrpcPort:                         DefaultGrpcPort,
-		PoolNamespacedName:               types.NamespacedName{Name: DefaultPoolName, Namespace: DefaultPoolNamespace},
-		PoolGKNN:                         poolGKNN,
+		GKNN:                             gknn,
+		DisableK8sCrdReconcile:           false,
 		SecureServing:                    DefaultSecureServing,
 		HealthChecking:                   DefaultHealthChecking,
 		RefreshPrometheusMetricsInterval: DefaultRefreshPrometheusMetricsInterval,
@@ -113,20 +113,22 @@ func NewDefaultExtProcServerRunner() *ExtProcServerRunner {
 // SetupWithManager sets up the runner with the given manager.
 func (r *ExtProcServerRunner) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
 	// Create the controllers and register them with the manager
-	if err := (&controller.InferencePoolReconciler{
-		Datastore: r.Datastore,
-		Reader:    mgr.GetClient(),
-		PoolGKNN:  r.PoolGKNN,
-	}).SetupWithManager(mgr); err != nil {
-		return fmt.Errorf("failed setting up InferencePoolReconciler: %w", err)
-	}
+	if !r.DisableK8sCrdReconcile {
+		if err := (&controller.InferencePoolReconciler{
+			Datastore: r.Datastore,
+			Reader:    mgr.GetClient(),
+			PoolGKNN:  r.GKNN,
+		}).SetupWithManager(mgr); err != nil {
+			return fmt.Errorf("failed setting up InferencePoolReconciler: %w", err)
+		}
 
-	if err := (&controller.InferenceObjectiveReconciler{
-		Datastore: r.Datastore,
-		Reader:    mgr.GetClient(),
-		PoolGKNN:  r.PoolGKNN,
-	}).SetupWithManager(ctx, mgr); err != nil {
-		return fmt.Errorf("failed setting up InferenceObjectiveReconciler: %w", err)
+		if err := (&controller.InferenceObjectiveReconciler{
+			Datastore: r.Datastore,
+			Reader:    mgr.GetClient(),
+			PoolGKNN:  r.GKNN,
+		}).SetupWithManager(ctx, mgr); err != nil {
+			return fmt.Errorf("failed setting up InferenceObjectiveReconciler: %w", err)
+		}
 	}
 
 	if err := (&controller.PodReconciler{
diff --git a/pkg/epp/util/pool/pool.go b/pkg/epp/util/pool/pool.go
new file mode 100644
index 0000000..67fd9fe
--- /dev/null
+++ b/pkg/epp/util/pool/pool.go
@@ -0,0 +1,89 @@
+/*
+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 pool
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	v1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
+)
+
+func InferencePoolToEndpointPool(inferencePool *v1.InferencePool) *datalayer.EndpointPool {
+	if inferencePool == nil {
+		return nil
+	}
+	targetPorts := make([]int, 0, len(inferencePool.Spec.TargetPorts))
+	for _, p := range inferencePool.Spec.TargetPorts {
+		targetPorts = append(targetPorts, int(p.Number))
+
+	}
+	selector := make(map[string]string, len(inferencePool.Spec.Selector.MatchLabels))
+	for k, v := range inferencePool.Spec.Selector.MatchLabels {
+		selector[string(k)] = string(v)
+	}
+	endpointPool := &datalayer.EndpointPool{
+		Selector:    selector,
+		TargetPorts: targetPorts,
+		Namespace:   inferencePool.Namespace,
+		Name:        inferencePool.Name,
+	}
+	return endpointPool
+}
+
+func AlphaInferencePoolToEndpointPool(inferencePool *v1alpha2.InferencePool) *datalayer.EndpointPool {
+	targetPorts := []int{int(inferencePool.Spec.TargetPortNumber)}
+	selector := make(map[string]string, len(inferencePool.Spec.Selector))
+	for k, v := range inferencePool.Spec.Selector {
+		selector[string(k)] = string(v)
+	}
+
+	endpointPool := &datalayer.EndpointPool{
+		TargetPorts: targetPorts,
+		Selector:    selector,
+		Namespace:   inferencePool.Namespace,
+		Name:        inferencePool.Name,
+	}
+	return endpointPool
+}
+
+func EndpointPoolToInferencePool(endpointPool *datalayer.EndpointPool) *v1.InferencePool {
+	targetPorts := make([]v1.Port, 0, len(endpointPool.TargetPorts))
+	for _, p := range endpointPool.TargetPorts {
+		targetPorts = append(targetPorts, v1.Port{Number: v1.PortNumber(p)})
+	}
+	labels := make(map[v1.LabelKey]v1.LabelValue, len(endpointPool.Selector))
+	for k, v := range endpointPool.Selector {
+		labels[v1.LabelKey(k)] = v1.LabelValue(v)
+	}
+
+	inferencePool := &v1.InferencePool{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "inference.networking.k8s.io/v1",
+			Kind:       "InferencePool",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      endpointPool.Name,
+			Namespace: endpointPool.Namespace,
+		},
+		Spec: v1.InferencePoolSpec{
+			Selector:    v1.LabelSelector{MatchLabels: labels},
+			TargetPorts: targetPorts,
+		},
+	}
+	return inferencePool
+}
diff --git a/site-src/performance/benchmark/advanced-configs/decode-heavy-workload.md b/site-src/performance/benchmark/advanced-configs/decode-heavy-workload.md
new file mode 100644
index 0000000..adec5bb
--- /dev/null
+++ b/site-src/performance/benchmark/advanced-configs/decode-heavy-workload.md
@@ -0,0 +1,97 @@
+# Decode Heavy Workload Benchmarking
+This guide shows how to deploy a decode-heavy benchmarking config using inference-perf.
+
+## Prerequisites
+
+Before you begin, ensure you have the following:
+
+*   **Helm 3+**: [Installation Guide](https://helm.sh/docs/intro/install/)
+*   **Kubernetes Cluster**: Access to a Kubernetes cluster
+*   **Hugging Face Token Secret**: A Hugging Face token to pull models.
+*   **Gateway Deployed**: Your inference server/gateway must be deployed and accessible within the cluster.
+
+Follow [benchmarking guide](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/#benchmark) for more information on how to set up gateway and how to validate benchmark results.
+
+## Infinity Instruct Dataset Configuration
+
+The chart uses the `infinity_instruct` [dataset type](https://huggingface.co/datasets/BAAI/Infinity-Instruct). 
+
+>NOTE: Currently, we need to download and supply the dataset for inference-perf to ingest. Currently using helm, we can supply the dataset by uploading to a gcs or s3 bucket. Otherwise, you can follow inference perf guides to run locally with a local dataset file path.
+
+## Deployment
+
+### 1. Check out the repo.
+
+```bash
+git clone https://github.com/kubernetes-sigs/gateway-api-inference-extension
+cd gateway-api-inference-extension/benchmarking/single-workload
+```
+
+### 2. Get the target IP. 
+
+  The examples below shows how to get the IP of a gateway or a k8s service.
+
+  ```bash
+  # Get gateway IP
+  GW_IP=$(kubectl get gateway/inference-gateway -o jsonpath='{.status.addresses[0].value}')
+  # Get LoadBalancer k8s service IP
+  SVC_IP=$(kubectl get service/vllm-llama3-8b-instruct -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+
+  echo $GW_IP
+  echo $SVC_IP
+  ```
+
+### 3. Deploying the Decode Heavy Configuration
+
+This configuration is optimized for scenarios where a high cache hit rate is expected. It uses the `decode-heavy-values.yaml` file.
+
+=== "Google Cloud Storage (GCS)"
+    Use the `gcsPath` field to provide your dataset stored on GCS. The dataset will be downloaded from the bucket and stored locally on the cluster at `/dataset/gcs-dataset.json`. 
+    ```bash
+    export IP='<YOUR_IP>'
+    export PORT='<YOUR_PORT>'
+    export HF_TOKEN='<YOUR_HUGGINGFACE_TOKEN>'
+    helm install decode-heavy ../inference-perf -f decode-heavy-values.yaml \
+      --set hfToken=${HF_TOKEN} \
+      --set "config.server.base_url=http://${IP}:${PORT}" \
+      --set "config.data.path=/dataset/gcs-dataset.json" \
+      --set "gcsPath=<PATH TO DATASET FILE ON GCS BUCKET>"
+    ```
+    **Parameters to customize:**
+    
+    *   `decode-heavy`: A unique name for this deployment.
+    *   `hfTokenSecret.name`: The name of your Kubernetes Secret containing the Hugging Face token (default: `hf-token`).
+    *   `hfTokenSecret.key`: The key in your Kubernetes Secret pointing to the Hugging Face token (default: `token`).
+    *   `config.server.base_url`: The base URL (IP and port) of your inference server for the high-cache scenario.
+    *   `gcsPath`: The path to the downloaded dataset file hosted on your gcs bucket. 
+
+=== "Simple Storage Service (S3)"
+    Use the `s3Path` field to provide your dataset stored on S3. The dataset will be downloaded from the bucket and stored locally on the cluster at `/dataset/s3-dataset.json`. 
+    ```bash
+    export IP='<YOUR_IP>'
+    export PORT='<YOUR_PORT>'
+    export HF_TOKEN='<YOUR_HUGGINGFACE_TOKEN>'
+    helm install decode-heavy ../inference-perf -f decode-heavy-values.yaml \
+      --set hfToken=${HF_TOKEN} \
+      --set "config.server.base_url=http://${IP}:${PORT}" \
+      --set "config.data.path=/dataset/s3-dataset.json" \
+      --set "s3Path=<PATH TO DATASET FILE ON S3 BUCKET>"
+    ```
+    **Parameters to customize:**
+    
+    *   `decode-heavy`: A unique name for this deployment.
+    *   `hfTokenSecret.name`: The name of your Kubernetes Secret containing the Hugging Face token (default: `hf-token`).
+    *   `hfTokenSecret.key`: The key in your Kubernetes Secret pointing to the Hugging Face token (default: `token`).
+    *   `config.server.base_url`: The base URL (IP and port) of your inference server for the high-cache scenario.
+    *   `s3Path`: The path to the downloaded dataset file hosted on your s3 bucket. 
+
+## Clean Up
+
+To uninstall the deployed charts:
+
+```bash
+helm uninstall decode-heavy
+```
+
+## Post Benchmark Analysis
+Follow the benchmarking guide instructions to [compare benchmark results](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/#analyze-the-results).
diff --git a/site-src/performance/benchmark/advanced-configs/prefill-heavy-workload.md b/site-src/performance/benchmark/advanced-configs/prefill-heavy-workload.md
new file mode 100644
index 0000000..d31f99f
--- /dev/null
+++ b/site-src/performance/benchmark/advanced-configs/prefill-heavy-workload.md
@@ -0,0 +1,98 @@
+# Prefill Heavy Workload Benchmarking
+This guide shows how to deploy a prefill-heavy benchmarking config using inference-perf.
+
+## Prerequisites
+
+Before you begin, ensure you have the following:
+
+*   **Helm 3+**: [Installation Guide](https://helm.sh/docs/intro/install/)
+*   **Kubernetes Cluster**: Access to a Kubernetes cluster
+*   **Hugging Face Token Secret**: A Hugging Face token to pull models.
+*   **Gateway Deployed**: Your inference server/gateway must be deployed and accessible within the cluster.
+
+Follow [benchmarking guide](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/#benchmark) for more information on how to set up gateway and how to validate benchmark results.
+
+## Infinity Instruct Dataset Configuration
+
+The chart uses the `infinity_instruct` [dataset type](https://huggingface.co/datasets/BAAI/Infinity-Instruct). 
+
+>NOTE: Currently, we need to download and supply the dataset for inference-perf to ingest. Currently using helm, we can supply the dataset by uploading to a gcs or s3 bucket. Otherwise, you can follow inference perf guides to run locally with a local dataset file path.
+
+## Deployment
+
+### 1. Check out the repo.
+
+```bash
+git clone https://github.com/kubernetes-sigs/gateway-api-inference-extension
+cd gateway-api-inference-extension/benchmarking/single-workload
+```
+
+### 2. Get the target IP. 
+
+  The examples below shows how to get the IP of a gateway or a k8s service.
+
+  ```bash
+  # Get gateway IP
+  GW_IP=$(kubectl get gateway/inference-gateway -o jsonpath='{.status.addresses[0].value}')
+  # Get LoadBalancer k8s service IP
+  SVC_IP=$(kubectl get service/vllm-llama3-8b-instruct -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
+
+  echo $GW_IP
+  echo $SVC_IP
+  ```
+
+### 3. Deploying the Prefill Heavy Configuration
+
+This configuration is optimized for scenarios where a high cache hit rate is expected. It uses the `prefill-heavy-values.yaml` file.
+
+=== "Google Cloud Storage (GCS)"
+    Use the `gcsPath` field to provide your dataset stored on GCS. The dataset will be downloaded from the bucket and stored locally on the cluster at `/dataset/gcs-dataset.json`. 
+    ```bash
+    export IP='<YOUR_IP>'
+    export PORT='<YOUR_PORT>'
+    export HF_TOKEN='<YOUR_HUGGINGFACE_TOKEN>'
+    helm install prefill-heavy ../inference-perf -f prefill-heavy-values.yaml \
+      --set hfToken=${HF_TOKEN} \
+      --set "config.server.base_url=http://${IP}:${PORT}" \
+      --set "config.data.path=/dataset/gcs-dataset.json" \
+      --set "gcsPath=<PATH TO DATASET FILE ON GCS BUCKET>"
+    ```
+    **Parameters to customize:**
+    
+    *   `prefill-heavy`: A unique name for this deployment.
+    *   `hfTokenSecret.name`: The name of your Kubernetes Secret containing the Hugging Face token (default: `hf-token`).
+    *   `hfTokenSecret.key`: The key in your Kubernetes Secret pointing to the Hugging Face token (default: `token`).
+    *   `config.server.base_url`: The base URL (IP and port) of your inference server for the high-cache scenario.
+    *   `gcsPath`: The path to the downloaded dataset file hosted on your gcs bucket. 
+
+=== "Simple Storage Service (S3)"
+    Use the `s3Path` field to provide your dataset stored on S3. The dataset will be downloaded from the bucket and stored locally on the cluster at `/dataset/s3-dataset.json`. 
+    ```bash
+    export IP='<YOUR_IP>'
+    export PORT='<YOUR_PORT>'
+    export HF_TOKEN='<YOUR_HUGGINGFACE_TOKEN>'
+    helm install prefill-heavy ../inference-perf -f prefill-heavy-values.yaml \
+      --set hfToken=${HF_TOKEN} \
+      --set "config.server.base_url=http://${IP}:${PORT}" \
+      --set "config.data.path=/dataset/s3-dataset.json" \
+      --set "s3Path=<PATH TO DATASET FILE ON S3 BUCKET>"
+    ```
+
+    **Parameters to customize:**
+    
+    *   `prefill-heavy`: A unique name for this deployment.
+    *   `hfTokenSecret.name`: The name of your Kubernetes Secret containing the Hugging Face token (default: `hf-token`).
+    *   `hfTokenSecret.key`: The key in your Kubernetes Secret pointing to the Hugging Face token (default: `token`).
+    *   `config.server.base_url`: The base URL (IP and port) of your inference server for the high-cache scenario.
+    *   `s3Path`: The path to the downloaded dataset file hosted on your s3 bucket. 
+
+## Clean Up
+
+To uninstall the deployed charts:
+
+```bash
+helm uninstall prefill-heavy
+```
+
+## Post Benchmark Analysis
+Follow the benchmarking guide instructions to [compare benchmark results](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/#analyze-the-results).
diff --git a/site-src/performance/benchmark/index.md b/site-src/performance/benchmark/index.md
index 3c4fdfb..c57b338 100644
--- a/site-src/performance/benchmark/index.md
+++ b/site-src/performance/benchmark/index.md
@@ -173,9 +173,11 @@ detailed list of configuration knobs.
 
 The following is a list of advanced configurations available.
 
-| Guides | Config | Directory | Config(s)
+| Guide | Directory | Config(s)
 | :--- | :--- | :--- | :--- |
 | [Prefix Cache Aware Guide](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/advanced-configs/prefix-cache-aware/#prefix-cache-aware-benchmarking) | `prefix-cache-aware` | `benchamrking/prefix-cache-aware`  | `high-cache-values.yaml` `low-cache-values.yaml` |
+| [Decode Heavy Guide](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/advanced-configs/decode-heavy) | `benchamrking/single-workload`  | `decode-heavy-values.yaml` |
+| [Prefill Heavy Guide](https://gateway-api-inference-extension.sigs.k8s.io/performance/benchmark/advanced-configs/prefill-heavy) | `benchamrking/single-workload`  | `prefill-heavy-values.yaml` |
 
 ## Analyze the results
 



Test patch:
diff --git a/pkg/epp/backend/metrics/pod_metrics_test.go b/pkg/epp/backend/metrics/pod_metrics_test.go
index b0297cd..8a5561c 100644
--- a/pkg/epp/backend/metrics/pod_metrics_test.go
+++ b/pkg/epp/backend/metrics/pod_metrics_test.go
@@ -25,7 +25,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/types"
 
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 )
 
@@ -86,8 +85,8 @@ func TestMetricsRefresh(t *testing.T) {
 
 type fakeDataStore struct{}
 
-func (f *fakeDataStore) PoolGet() (*v1.InferencePool, error) {
-	return &v1.InferencePool{Spec: v1.InferencePoolSpec{TargetPorts: []v1.Port{{Number: 8000}}}}, nil
+func (f *fakeDataStore) PoolGet() (*datalayer.EndpointPool, error) {
+	return &datalayer.EndpointPool{}, nil
 }
 
 func (f *fakeDataStore) PodList(func(PodMetrics) bool) []PodMetrics {
diff --git a/pkg/epp/controller/inferenceobjective_reconciler_test.go b/pkg/epp/controller/inferenceobjective_reconciler_test.go
index 4ceff5d..f8d48ec 100644
--- a/pkg/epp/controller/inferenceobjective_reconciler_test.go
+++ b/pkg/epp/controller/inferenceobjective_reconciler_test.go
@@ -27,25 +27,26 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
+	poolutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 	utiltest "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 )
 
 var (
-	pool          = utiltest.MakeInferencePool("test-pool1").Namespace("ns1").ObjRef()
+	inferencePool = utiltest.MakeInferencePool("test-pool1").Namespace("ns1").ObjRef()
 	infObjective1 = utiltest.MakeInferenceObjective("model1").
-			Namespace(pool.Namespace).
+			Namespace(inferencePool.Namespace).
 			Priority(1).
 			CreationTimestamp(metav1.Unix(1000, 0)).
-			PoolName(pool.Name).
+			PoolName(inferencePool.Name).
 			PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1Pool2 = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(infObjective1.Namespace).
@@ -57,24 +58,24 @@ var (
 				Namespace(infObjective1.Namespace).
 				Priority(2).
 				CreationTimestamp(metav1.Unix(1003, 0)).
-				PoolName(pool.Name).
+				PoolName(inferencePool.Name).
 				PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1Deleted = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(infObjective1.Namespace).
 				CreationTimestamp(metav1.Unix(1004, 0)).
 				DeletionTimestamp().
-				PoolName(pool.Name).
+				PoolName(inferencePool.Name).
 				PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1DiffGroup = utiltest.MakeInferenceObjective(infObjective1.Name).
-				Namespace(pool.Namespace).
+				Namespace(inferencePool.Namespace).
 				Priority(1).
 				CreationTimestamp(metav1.Unix(1005, 0)).
-				PoolName(pool.Name).
+				PoolName(inferencePool.Name).
 				PoolGroup("inference.networking.x-k8s.io").ObjRef()
 	infObjective2 = utiltest.MakeInferenceObjective("model2").
-			Namespace(pool.Namespace).
+			Namespace(inferencePool.Namespace).
 			CreationTimestamp(metav1.Unix(1000, 0)).
-			PoolName(pool.Name).
+			PoolName(inferencePool.Name).
 			PoolGroup("inference.networking.k8s.io").ObjRef()
 )
 
@@ -120,7 +121,7 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 		{
 			name:               "Objective not found, no matching existing objective to delete",
 			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
-			incomingReq:        &types.NamespacedName{Name: "non-existent-objective", Namespace: pool.Namespace},
+			incomingReq:        &types.NamespacedName{Name: "non-existent-objective", Namespace: inferencePool.Namespace},
 			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1},
 		},
 		{
@@ -130,13 +131,13 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1, infObjective2},
 		},
 		{
-			name:               "Objective deleted due to group mismatch for the inference pool",
+			name:               "Objective deleted due to group mismatch for the inference inferencePool",
 			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
 			objective:          infObjective1DiffGroup,
 			wantObjectives:     []*v1alpha2.InferenceObjective{},
 		},
 		{
-			name:           "Objective ignored due to group mismatch for the inference pool",
+			name:           "Objective ignored due to group mismatch for the inference inferencePool",
 			objective:      infObjective1DiffGroup,
 			wantObjectives: []*v1alpha2.InferenceObjective{},
 		},
@@ -164,13 +165,14 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			for _, m := range test.objectivessInStore {
 				ds.ObjectiveSet(m)
 			}
-			_ = ds.PoolSet(context.Background(), fakeClient, pool)
+			endpointPool := poolutil.InferencePoolToEndpointPool(inferencePool)
+			_ = ds.PoolSet(context.Background(), fakeClient, endpointPool)
 			reconciler := &InferenceObjectiveReconciler{
 				Reader:    fakeClient,
 				Datastore: ds,
 				PoolGKNN: common.GKNN{
-					NamespacedName: types.NamespacedName{Name: pool.Name, Namespace: pool.Namespace},
-					GroupKind:      schema.GroupKind{Group: pool.GroupVersionKind().Group, Kind: pool.GroupVersionKind().Kind},
+					NamespacedName: types.NamespacedName{Name: inferencePool.Name, Namespace: inferencePool.Namespace},
+					GroupKind:      schema.GroupKind{Group: inferencePool.GroupVersionKind().Group, Kind: inferencePool.GroupVersionKind().Kind},
 				},
 			}
 			if test.incomingReq == nil {
@@ -190,8 +192,7 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			if len(test.wantObjectives) != len(ds.ObjectiveGetAll()) {
 				t.Errorf("Unexpected; want: %d, got:%d", len(test.wantObjectives), len(ds.ObjectiveGetAll()))
 			}
-
-			if diff := diffStore(ds, diffStoreParams{wantPool: pool, wantObjectives: test.wantObjectives}); diff != "" {
+			if diff := diffStore(ds, diffStoreParams{wantPool: endpointPool, wantObjectives: test.wantObjectives}); diff != "" {
 				t.Errorf("Unexpected diff (+got/-want): %s", diff)
 			}
 
diff --git a/pkg/epp/controller/inferencepool_reconciler_test.go b/pkg/epp/controller/inferencepool_reconciler_test.go
index a2bce12..0fdecb6 100644
--- a/pkg/epp/controller/inferencepool_reconciler_test.go
+++ b/pkg/epp/controller/inferencepool_reconciler_test.go
@@ -24,20 +24,21 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 	utiltest "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 )
 
@@ -121,49 +122,51 @@ func TestInferencePoolReconciler(t *testing.T) {
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := diffStore(ds, diffStoreParams{wantPool: pool1, wantPods: []string{"pod1-rank-0", "pod2-rank-0"}}); diff != "" {
+	endpointPool1 := pool.InferencePoolToEndpointPool(pool1)
+	if diff := diffStore(ds, diffStoreParams{wantPool: endpointPool1, wantPods: []string{"pod1-rank-0", "pod2-rank-0"}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 
 	newPool1 := &v1.InferencePool{}
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
-		t.Errorf("Unexpected pool get error: %v", err)
+		t.Errorf("Unexpected inferencePool get error: %v", err)
 	}
 	newPool1.Spec.Selector = v1.LabelSelector{
 		MatchLabels: map[v1.LabelKey]v1.LabelValue{"app": "vllm_v2"},
 	}
 	if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil {
-		t.Errorf("Unexpected pool update error: %v", err)
+		t.Errorf("Unexpected inferencePool update error: %v", err)
 	}
-
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := diffStore(ds, diffStoreParams{wantPool: newPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
+	newEndpointPool1 := pool.InferencePoolToEndpointPool(newPool1)
+	if diff := diffStore(ds, diffStoreParams{wantPool: newEndpointPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 
-	// Step 3: update the pool port
+	// Step 3: update the inferencePool port
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
-		t.Errorf("Unexpected pool get error: %v", err)
+		t.Errorf("Unexpected inferencePool get error: %v", err)
 	}
 	newPool1.Spec.TargetPorts = []v1.Port{{Number: 9090}}
 	if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil {
-		t.Errorf("Unexpected pool update error: %v", err)
+		t.Errorf("Unexpected inferencePool update error: %v", err)
 	}
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := diffStore(ds, diffStoreParams{wantPool: newPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
+	newEndpointPool1 = pool.InferencePoolToEndpointPool(newPool1)
+	if diff := diffStore(ds, diffStoreParams{wantPool: newEndpointPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 
-	// Step 4: delete the pool to trigger a datastore clear
+	// Step 4: delete the inferencePool to trigger a datastore clear
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
-		t.Errorf("Unexpected pool get error: %v", err)
+		t.Errorf("Unexpected inferencePool get error: %v", err)
 	}
 	if err := fakeClient.Delete(ctx, newPool1, &client.DeleteOptions{}); err != nil {
-		t.Errorf("Unexpected pool delete error: %v", err)
+		t.Errorf("Unexpected inferencePool delete error: %v", err)
 	}
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
@@ -174,17 +177,15 @@ func TestInferencePoolReconciler(t *testing.T) {
 }
 
 type diffStoreParams struct {
-	wantPool       *v1.InferencePool
+	wantPool       *datalayer.EndpointPool
 	wantPods       []string
 	wantObjectives []*v1alpha2.InferenceObjective
 }
 
 func diffStore(datastore datastore.Datastore, params diffStoreParams) string {
 	gotPool, _ := datastore.PoolGet()
-	// controller-runtime fake client may not populate TypeMeta (APIVersion/Kind).
-	// Ignore it when comparing pools.
-	if diff := cmp.Diff(params.wantPool, gotPool, cmpopts.IgnoreTypes(metav1.TypeMeta{})); diff != "" {
-		return "pool:" + diff
+	if diff := cmp.Diff(params.wantPool, gotPool); diff != "" {
+		return "inferencePool:" + diff
 	}
 
 	// Default wantPods if not set because PodGetAll returns an empty slice when empty.
@@ -268,79 +269,73 @@ func TestXInferencePoolReconciler(t *testing.T) {
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := xDiffStore(t, ds, xDiffStoreParams{wantPool: pool1, wantPods: []string{"pod1-rank-0", "pod2-rank-0"}}); diff != "" {
+	endpointPool1 := pool.AlphaInferencePoolToEndpointPool(pool1)
+	if diff := xDiffStore(ds, xDiffStoreParams{wantPool: endpointPool1, wantPods: []string{"pod1-rank-0", "pod2-rank-0"}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 
 	newPool1 := &v1alpha2.InferencePool{}
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
-		t.Errorf("Unexpected pool get error: %v", err)
+		t.Errorf("Unexpected inferencePool get error: %v", err)
 	}
 	newPool1.Spec.Selector = map[v1alpha2.LabelKey]v1alpha2.LabelValue{"app": "vllm_v2"}
 	if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil {
-		t.Errorf("Unexpected pool update error: %v", err)
+		t.Errorf("Unexpected inferencePool update error: %v", err)
 	}
 
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := xDiffStore(t, ds, xDiffStoreParams{wantPool: newPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
+	newEndpointPool1 := pool.AlphaInferencePoolToEndpointPool(newPool1)
+	if diff := xDiffStore(ds, xDiffStoreParams{wantPool: newEndpointPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 
-	// Step 3: update the pool port
+	// Step 3: update the inferencePool port
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
-		t.Errorf("Unexpected pool get error: %v", err)
+		t.Errorf("Unexpected inferencePool get error: %v", err)
 	}
 	newPool1.Spec.TargetPortNumber = 9090
 	if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil {
-		t.Errorf("Unexpected pool update error: %v", err)
+		t.Errorf("Unexpected inferencePool update error: %v", err)
 	}
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := xDiffStore(t, ds, xDiffStoreParams{wantPool: newPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
+	newEndpointPool1 = pool.AlphaInferencePoolToEndpointPool(newPool1)
+	if diff := xDiffStore(ds, xDiffStoreParams{wantPool: newEndpointPool1, wantPods: []string{"pod5-rank-0"}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 
-	// Step 4: delete the pool to trigger a datastore clear
+	// Step 4: delete the inferencePool to trigger a datastore clear
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
-		t.Errorf("Unexpected pool get error: %v", err)
+		t.Errorf("Unexpected inferencePool get error: %v", err)
 	}
 	if err := fakeClient.Delete(ctx, newPool1, &client.DeleteOptions{}); err != nil {
-		t.Errorf("Unexpected pool delete error: %v", err)
+		t.Errorf("Unexpected inferencePool delete error: %v", err)
 	}
 	if _, err := inferencePoolReconciler.Reconcile(ctx, req); err != nil {
 		t.Errorf("Unexpected InferencePool reconcile error: %v", err)
 	}
-	if diff := xDiffStore(t, ds, xDiffStoreParams{wantPods: []string{}}); diff != "" {
+	if diff := xDiffStore(ds, xDiffStoreParams{wantPods: []string{}}); diff != "" {
 		t.Errorf("Unexpected diff (+got/-want): %s", diff)
 	}
 }
 
 type xDiffStoreParams struct {
-	wantPool       *v1alpha2.InferencePool
+	wantPool       *datalayer.EndpointPool
 	wantPods       []string
 	wantObjectives []*v1alpha2.InferenceObjective
 }
 
-func xDiffStore(t *testing.T, datastore datastore.Datastore, params xDiffStoreParams) string {
+func xDiffStore(datastore datastore.Datastore, params xDiffStoreParams) string {
 	gotPool, _ := datastore.PoolGet()
 	if gotPool == nil && params.wantPool == nil {
 		return ""
 	}
 
-	gotXPool := &v1alpha2.InferencePool{}
-
-	err := gotXPool.ConvertFrom(gotPool)
-	if err != nil {
-		t.Fatalf("failed to convert InferencePool to XInferencePool: %v", err)
-	}
-
-	// controller-runtime fake client may not populate TypeMeta (APIVersion/Kind).
-	// Ignore it when comparing pools.
-	if diff := cmp.Diff(params.wantPool, gotXPool, cmpopts.IgnoreTypes(metav1.TypeMeta{})); diff != "" {
-		return "pool:" + diff
+	if diff := cmp.Diff(params.wantPool, gotPool); diff != "" {
+		return "inferencePool:" + diff
 	}
 
 	// Default wantPods if not set because PodGetAll returns an empty slice when empty.
diff --git a/pkg/epp/controller/pod_reconciler_test.go b/pkg/epp/controller/pod_reconciler_test.go
index 28f8173..efdb36b 100644
--- a/pkg/epp/controller/pod_reconciler_test.go
+++ b/pkg/epp/controller/pod_reconciler_test.go
@@ -28,13 +28,14 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	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/util/pool"
 	utiltest "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 )
 
@@ -197,7 +198,7 @@ func TestPodReconciler(t *testing.T) {
 
 			// Configure the initial state of the datastore.
 			store := datastore.NewDatastore(t.Context(), pmf, 0)
-			_ = store.PoolSet(t.Context(), fakeClient, test.pool)
+			_ = store.PoolSet(t.Context(), fakeClient, pool.InferencePoolToEndpointPool(test.pool))
 			for _, pod := range test.existingPods {
 				store.PodUpdateOrAddIfNotExist(pod)
 			}
diff --git a/pkg/epp/datalayer/metrics/logger_test.go b/pkg/epp/datalayer/metrics/logger_test.go
index 3ba2e7e..4bf68cf 100644
--- a/pkg/epp/datalayer/metrics/logger_test.go
+++ b/pkg/epp/datalayer/metrics/logger_test.go
@@ -31,6 +31,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
+	poolutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 )
 
 // Buffer to write the logs to
@@ -95,8 +96,9 @@ var pod2 = &datalayer.PodInfo{
 
 type fakeDataStore struct{}
 
-func (f *fakeDataStore) PoolGet() (*v1.InferencePool, error) {
-	return &v1.InferencePool{Spec: v1.InferencePoolSpec{TargetPorts: []v1.Port{{Number: 8000}}}}, nil
+func (f *fakeDataStore) PoolGet() (*datalayer.EndpointPool, error) {
+	pool := &v1.InferencePool{Spec: v1.InferencePoolSpec{TargetPorts: []v1.Port{{Number: 8000}}}}
+	return poolutil.InferencePoolToEndpointPool(pool), nil
 }
 
 func (f *fakeDataStore) PodList(predicate func(datalayer.Endpoint) bool) []datalayer.Endpoint {
diff --git a/pkg/epp/datastore/datastore_test.go b/pkg/epp/datastore/datastore_test.go
index ee59071..73beb1f 100644
--- a/pkg/epp/datastore/datastore_test.go
+++ b/pkg/epp/datastore/datastore_test.go
@@ -32,12 +32,13 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
+	pooltuil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 	testutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 )
 
@@ -87,12 +88,12 @@ func TestPool(t *testing.T) {
 				Build()
 			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			ds := NewDatastore(context.Background(), pmf, 0)
-			_ = ds.PoolSet(context.Background(), fakeClient, tt.inferencePool)
+			_ = ds.PoolSet(context.Background(), fakeClient, pooltuil.InferencePoolToEndpointPool(tt.inferencePool))
 			gotPool, gotErr := ds.PoolGet()
 			if diff := cmp.Diff(tt.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" {
 				t.Errorf("Unexpected error diff (+got/-want): %s", diff)
 			}
-			if diff := cmp.Diff(tt.wantPool, gotPool); diff != "" {
+			if diff := cmp.Diff(pooltuil.InferencePoolToEndpointPool(tt.wantPool), gotPool); diff != "" {
 				t.Errorf("Unexpected pool diff (+got/-want): %s", diff)
 			}
 			gotSynced := ds.PoolHasSynced()
@@ -328,7 +329,7 @@ func TestMetrics(t *testing.T) {
 				Build()
 			pmf := backendmetrics.NewPodMetricsFactory(test.pmc, time.Millisecond)
 			ds := NewDatastore(ctx, pmf, 0)
-			_ = ds.PoolSet(ctx, fakeClient, inferencePool)
+			_ = ds.PoolSet(ctx, fakeClient, pooltuil.InferencePoolToEndpointPool(inferencePool))
 			for _, pod := range test.storePods {
 				ds.PodUpdateOrAddIfNotExist(pod)
 			}
@@ -397,7 +398,7 @@ func TestPods(t *testing.T) {
 			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			ds := NewDatastore(t.Context(), pmf, 0)
 			fakeClient := fake.NewFakeClient()
-			if err := ds.PoolSet(ctx, fakeClient, inferencePool); err != nil {
+			if err := ds.PoolSet(ctx, fakeClient, pooltuil.InferencePoolToEndpointPool(inferencePool)); err != nil {
 				t.Error(err)
 			}
 			for _, pod := range test.existingPods {
@@ -581,7 +582,7 @@ func TestPodInfo(t *testing.T) {
 			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			ds := NewDatastore(t.Context(), pmf, 0)
 			fakeClient := fake.NewFakeClient()
-			if err := ds.PoolSet(ctx, fakeClient, test.pool); err != nil {
+			if err := ds.PoolSet(ctx, fakeClient, pooltuil.InferencePoolToEndpointPool(test.pool)); err != nil {
 				t.Error(err)
 			}
 			for _, pod := range test.existingPods {
diff --git a/pkg/epp/flowcontrol/controller/internal/processor_test.go b/pkg/epp/flowcontrol/controller/internal/processor_test.go
index a4efd3c..ff75130 100644
--- a/pkg/epp/flowcontrol/controller/internal/processor_test.go
+++ b/pkg/epp/flowcontrol/controller/internal/processor_test.go
@@ -480,11 +480,23 @@ func TestShardProcessor(t *testing.T) {
 			defer h.Stop()
 			h.Go()
 
-			// Wait for the dispatch cycle to select our item and pause inside our mock `RemoveFunc`.
-			select {
-			case <-itemIsBeingDispatched:
-			case <-time.After(testWaitTimeout):
-				t.Fatal("Timed out waiting for item to be dispatched")
+			// Advance the test clock in small increments until the item is being dispatched or timeout
+			// This is a more reliable way to ensure the processor has started and run the dispatch cycle
+			timeout := time.After(testWaitTimeout)
+			ticker := time.NewTicker(1 * time.Millisecond)
+			defer ticker.Stop()
+
+			dispatched := false
+			for !dispatched {
+				select {
+				case <-itemIsBeingDispatched:
+					dispatched = true
+				case <-timeout:
+					t.Fatal("Timed out waiting for item to be dispatched")
+				case <-ticker.C:
+					// Advance the test clock to trigger the dispatch ticker
+					h.clock.Step(1 * time.Millisecond)
+				}
 			}
 
 			// 3. The dispatch goroutine is now paused. We can now safely win the "race" by running cleanup logic.
diff --git a/pkg/epp/metrics/collectors/inference_pool_test.go b/pkg/epp/metrics/collectors/inference_pool_test.go
index af2923e..20ab69f 100644
--- a/pkg/epp/metrics/collectors/inference_pool_test.go
+++ b/pkg/epp/metrics/collectors/inference_pool_test.go
@@ -27,11 +27,12 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/component-base/metrics/testutil"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
+	poolutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 )
 
 var (
@@ -68,13 +69,6 @@ func TestMetricsCollected(t *testing.T) {
 		},
 	}
 	pmf := backendmetrics.NewPodMetricsFactory(pmc, time.Millisecond)
-	ds := datastore.NewDatastore(context.Background(), pmf, 0)
-
-	scheme := runtime.NewScheme()
-	fakeClient := fake.NewClientBuilder().
-		WithScheme(scheme).
-		Build()
-
 	inferencePool := &v1.InferencePool{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "test-pool",
@@ -83,7 +77,14 @@ func TestMetricsCollected(t *testing.T) {
 			TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
 		},
 	}
-	_ = ds.PoolSet(context.Background(), fakeClient, inferencePool)
+	ds := datastore.NewDatastore(context.Background(), pmf, 0)
+
+	scheme := runtime.NewScheme()
+	fakeClient := fake.NewClientBuilder().
+		WithScheme(scheme).
+		Build()
+
+	_ = ds.PoolSet(context.Background(), fakeClient, poolutil.InferencePoolToEndpointPool(inferencePool))
 	_ = ds.PodUpdateOrAddIfNotExist(pod1)
 
 	time.Sleep(1 * time.Second)
diff --git a/pkg/epp/metrics/metrics_test.go b/pkg/epp/metrics/metrics_test.go
index 7d41681..754d6d2 100644
--- a/pkg/epp/metrics/metrics_test.go
+++ b/pkg/epp/metrics/metrics_test.go
@@ -46,6 +46,8 @@ const (
 	KVCacheAvgUsageMetric              = InferencePoolComponent + "_average_kv_cache_utilization"
 	QueueAvgSizeMetric                 = InferencePoolComponent + "_average_queue_size"
 	PerPodQueueSizeMetrics             = InferencePoolComponent + "_per_pod_queue_size"
+	RequestTTFTSecondsMetric           = InferenceObjectiveComponent + "_request_ttft_seconds"
+	RequestTPOTSecondsMetric           = InferenceObjectiveComponent + "_request_tpot_seconds"
 )
 
 func TestMain(m *testing.M) {
diff --git a/pkg/epp/requestcontrol/director_test.go b/pkg/epp/requestcontrol/director_test.go
index e705bee..f361303 100644
--- a/pkg/epp/requestcontrol/director_test.go
+++ b/pkg/epp/requestcontrol/director_test.go
@@ -32,8 +32,8 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
@@ -46,6 +46,7 @@ import (
 	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"
+	poolutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
 	testutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 )
@@ -89,7 +90,7 @@ type mockDatastore struct {
 	pods []backendmetrics.PodMetrics
 }
 
-func (ds *mockDatastore) PoolGet() (*v1.InferencePool, error) {
+func (ds *mockDatastore) PoolGet() (*datalayer.EndpointPool, error) {
 	return nil, nil
 }
 func (ds *mockDatastore) ObjectiveGet(_ string) *v1alpha2.InferenceObjective {
@@ -190,14 +191,6 @@ func TestDirector_HandleRequest(t *testing.T) {
 		CreationTimestamp(metav1.Unix(1000, 0)).
 		Priority(1).
 		ObjRef()
-
-	// Datastore setup
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
-	ds := datastore.NewDatastore(t.Context(), pmf, 0)
-	ds.ObjectiveSet(ioFoodReview)
-	ds.ObjectiveSet(ioFoodReviewResolve)
-	ds.ObjectiveSet(ioFoodReviewSheddable)
-
 	pool := &v1.InferencePool{
 		ObjectMeta: metav1.ObjectMeta{Name: "test-pool", Namespace: "default"},
 		Spec: v1.InferencePoolSpec{
@@ -210,10 +203,18 @@ func TestDirector_HandleRequest(t *testing.T) {
 		},
 	}
 
+	// Datastore setup
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
+	ds := datastore.NewDatastore(t.Context(), pmf, 0)
+	ds.ObjectiveSet(ioFoodReview)
+	ds.ObjectiveSet(ioFoodReviewResolve)
+	ds.ObjectiveSet(ioFoodReviewSheddable)
+
 	scheme := runtime.NewScheme()
 	_ = clientgoscheme.AddToScheme(scheme)
 	fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
-	if err := ds.PoolSet(ctx, fakeClient, pool); err != nil {
+
+	if err := ds.PoolSet(ctx, fakeClient, poolutil.InferencePoolToEndpointPool(pool)); err != nil {
 		t.Fatalf("Error while setting inference pool: %v", err)
 	}
 
@@ -754,8 +755,9 @@ func TestGetRandomPod(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Millisecond)
+			endpointPool := poolutil.InferencePoolToEndpointPool(pool)
 			ds := datastore.NewDatastore(t.Context(), pmf, 0)
-			err := ds.PoolSet(t.Context(), fakeClient, pool)
+			err := ds.PoolSet(t.Context(), fakeClient, endpointPool)
 			if err != nil {
 				t.Errorf("unexpected error setting pool: %s", err)
 			}
diff --git a/pkg/epp/requestcontrol/plugins/test/consts.go b/pkg/epp/requestcontrol/plugins/test/consts.go
new file mode 100644
index 0000000..194ef07
--- /dev/null
+++ b/pkg/epp/requestcontrol/plugins/test/consts.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+const (
+	// ConformanceTestResultHeader is the header key for the conformance test result.
+	ConformanceTestResultHeader = "x-conformance-test-served-endpoint"
+)
diff --git a/pkg/epp/requestcontrol/plugins/test/responsereceived/destination_endpoint_served_verifier.go b/pkg/epp/requestcontrol/plugins/test/responsereceived/destination_endpoint_served_verifier.go
new file mode 100644
index 0000000..216746a
--- /dev/null
+++ b/pkg/epp/requestcontrol/plugins/test/responsereceived/destination_endpoint_served_verifier.go
@@ -0,0 +1,93 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package responsereceived
+
+import (
+	"context"
+	"encoding/json"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol/plugins/test"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+const (
+	// DestinationEndpointServedVerifierType is the ResponseReceived type that is used in plugins registry.
+	DestinationEndpointServedVerifierType = "destination-endpoint-served-verifier"
+)
+
+var _ requestcontrol.ResponseReceived = &DestinationEndpointServedVerifier{}
+
+// DestinationEndpointServedVerifier is a test-only plugin for conformance tests.
+// It verifies that the request was served by the expected endpoint.
+// It works by reading Envoy's dynamic metadata, which is passed in the
+// Response.ReqMetadata field. This metadata should contain the
+// address of the backend endpoint that served the request. The verifier then
+// writes this address to the "x-conformance-test-served-endpoint" response header.
+// The conformance test client can then validate this header to ensure the request
+// was routed correctly.
+type DestinationEndpointServedVerifier struct {
+	typedName plugins.TypedName
+}
+
+// TypedName returns the type and name tuple of this plugin instance.
+func (f *DestinationEndpointServedVerifier) TypedName() plugins.TypedName {
+	return f.typedName
+}
+
+// WithName sets the name of the filter.
+func (f *DestinationEndpointServedVerifier) WithName(name string) *DestinationEndpointServedVerifier {
+	f.typedName.Name = name
+	return f
+}
+
+// DestinationEndpointServedVerifierFactory defines the factory function for DestinationEndpointServedVerifier.
+func DestinationEndpointServedVerifierFactory(name string, _ json.RawMessage, _ plugins.Handle) (plugins.Plugin, error) {
+	return NewDestinationEndpointServedVerifier().WithName(name), nil
+}
+
+func NewDestinationEndpointServedVerifier() *DestinationEndpointServedVerifier {
+	return &DestinationEndpointServedVerifier{}
+}
+
+// ResponseReceived is the handler for the ResponseReceived extension point.
+func (p *DestinationEndpointServedVerifier) ResponseReceived(ctx context.Context, request *schedulingtypes.LLMRequest, response *requestcontrol.Response, _ *backend.Pod) {
+	logger := log.FromContext(ctx).WithName(p.TypedName().String())
+	logger.V(logging.DEBUG).Info("Verifying destination endpoint served")
+
+	reqMetadata := response.ReqMetadata
+	lbMetadata, ok := reqMetadata[metadata.DestinationEndpointNamespace].(map[string]any)
+	if !ok {
+		logger.V(logging.DEBUG).Info("Response does not contain envoy lb metadata, skipping verification")
+		response.Headers[test.ConformanceTestResultHeader] = "fail: missing envoy lb metadata"
+		return
+	}
+
+	actualEndpoint, ok := lbMetadata[metadata.DestinationEndpointServedKey].(string)
+	if !ok {
+		logger.V(logging.DEBUG).Info("Response does not contain destination endpoint served metadata, skipping verification")
+		response.Headers[test.ConformanceTestResultHeader] = "fail: missing destination endpoint served metadata"
+		return
+	}
+	response.Headers[test.ConformanceTestResultHeader] = actualEndpoint
+}
diff --git a/pkg/epp/requestcontrol/plugins/test/responsereceived/destination_endpoint_served_verifier_test.go b/pkg/epp/requestcontrol/plugins/test/responsereceived/destination_endpoint_served_verifier_test.go
new file mode 100644
index 0000000..b5e6c94
--- /dev/null
+++ b/pkg/epp/requestcontrol/plugins/test/responsereceived/destination_endpoint_served_verifier_test.go
@@ -0,0 +1,88 @@
+/*
+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 responsereceived
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol/plugins/test"
+)
+
+func TestDestinationEndpointServedVerifier_ResponseReceived(t *testing.T) {
+	testCases := []struct {
+		name                string
+		response            *requestcontrol.Response
+		expectedHeaderValue string
+	}{
+		{
+			name: "success - endpoint is correctly reported",
+			response: &requestcontrol.Response{
+				Headers: make(map[string]string),
+				ReqMetadata: map[string]any{
+					metadata.DestinationEndpointNamespace: map[string]any{
+						metadata.DestinationEndpointServedKey: "10.0.0.1:8080",
+					},
+				},
+			},
+			expectedHeaderValue: "10.0.0.1:8080",
+		},
+		{
+			name: "failure - missing lb metadata",
+			response: &requestcontrol.Response{
+				Headers:     make(map[string]string),
+				ReqMetadata: map[string]any{},
+			},
+			expectedHeaderValue: "fail: missing envoy lb metadata",
+		},
+		{
+			name: "failure - missing served endpoint key",
+			response: &requestcontrol.Response{
+				Headers: make(map[string]string),
+				ReqMetadata: map[string]any{
+					metadata.DestinationEndpointNamespace: map[string]any{
+						"some-other-key": "some-value",
+					},
+				},
+			},
+			expectedHeaderValue: "fail: missing destination endpoint served metadata",
+		},
+		{
+			name: "failure - nil metadata",
+			response: &requestcontrol.Response{
+				Headers:     make(map[string]string),
+				ReqMetadata: nil,
+			},
+			expectedHeaderValue: "fail: missing envoy lb metadata",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			plugin := NewDestinationEndpointServedVerifier().WithName("test-verifier")
+
+			plugin.ResponseReceived(context.Background(), nil, tc.response, nil)
+
+			actualHeader, ok := tc.response.Headers[test.ConformanceTestResultHeader]
+			require.True(t, ok, "Expected header %s to be set", test.ConformanceTestResultHeader)
+			require.Equal(t, tc.expectedHeaderValue, actualHeader)
+		})
+	}
+}
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 1bb0794..f0feeef 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
@@ -35,7 +35,7 @@ import (
 
 func TestPrefixPluginCompletion(t *testing.T) {
 	config := Config{
-		DefaultBlockSize:       4,
+		BlockSize:              4,
 		MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
@@ -201,7 +201,7 @@ func TestPrefixPluginCompletion(t *testing.T) {
 
 func TestPrefixPluginChatCompletions(t *testing.T) {
 	config := Config{
-		DefaultBlockSize:       4,
+		BlockSize:              4,
 		MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
@@ -235,7 +235,7 @@ func TestPrefixPluginChatCompletions(t *testing.T) {
 
 func TestPrefixPluginChatCompletionsGrowth(t *testing.T) {
 	config := Config{
-		DefaultBlockSize:       8, // Use larger block size for more predictable JSON marshaling
+		BlockSize:              8, // Use larger block size for more predictable JSON marshaling
 		MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
@@ -349,7 +349,7 @@ func BenchmarkPrefixPluginStress(b *testing.B) {
 	blockSize := 4
 	maxPrefixBlocks := 50000
 	config := Config{
-		DefaultBlockSize:       blockSize,
+		BlockSize:              blockSize,
 		MaxPrefixBlocksToMatch: maxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
@@ -409,7 +409,7 @@ func TestNew_InvalidConfigFallbacks(t *testing.T) {
 		{
 			name: "all zero",
 			config: Config{
-				DefaultBlockSize:       0,
+				BlockSize:              0,
 				MaxPrefixBlocksToMatch: 0,
 				LRUCapacityPerServer:   0,
 			},
@@ -420,7 +420,7 @@ func TestNew_InvalidConfigFallbacks(t *testing.T) {
 		{
 			name: "negative values",
 			config: Config{
-				DefaultBlockSize:       -5,
+				BlockSize:              -5,
 				MaxPrefixBlocksToMatch: -10,
 				LRUCapacityPerServer:   -100,
 			},
@@ -431,7 +431,7 @@ func TestNew_InvalidConfigFallbacks(t *testing.T) {
 		{
 			name: "mixed valid and invalid",
 			config: Config{
-				DefaultBlockSize:       32,    // valid
+				BlockSize:              32,    // valid
 				MaxPrefixBlocksToMatch: -1,    // invalid
 				LRUCapacityPerServer:   50000, // valid
 			},
@@ -442,7 +442,7 @@ func TestNew_InvalidConfigFallbacks(t *testing.T) {
 		{
 			name: "all valid",
 			config: Config{
-				DefaultBlockSize:       64,
+				BlockSize:              64,
 				MaxPrefixBlocksToMatch: 200,
 				LRUCapacityPerServer:   30000,
 			},
@@ -459,13 +459,108 @@ func TestNew_InvalidConfigFallbacks(t *testing.T) {
 
 			assert.NotEmpty(t, plugin)
 			assert.NotEmpty(t, plugin.indexer)
-			assert.Equal(t, tt.expectBlock, plugin.config.DefaultBlockSize)
+			assert.Equal(t, tt.expectBlock, plugin.config.BlockSize)
 			assert.Equal(t, tt.expectMaxMatch, plugin.config.MaxPrefixBlocksToMatch)
 			assert.Equal(t, tt.expectCapacity, plugin.config.LRUCapacityPerServer)
 		})
 	}
 }
 
+func TestPrefixPluginAutoTune(t *testing.T) {
+	// Setup common test data
+	podName := "pod-autotune"
+	pod := &types.PodMetrics{
+		Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: podName}},
+		MetricsState: &backendmetrics.MetricsState{
+			CacheBlockSize:    16,   // 16 tokens * 4 chars/token = 64 chars per block
+			CacheNumGPUBlocks: 1000, // 1000 blocks capacity
+		},
+	}
+	pods := []types.Pod{pod}
+
+	req := &types.LLMRequest{
+		RequestId:   uuid.NewString(),
+		TargetModel: "test-model",
+		Body: &types.LLMRequestBody{
+			Completions: &types.CompletionsRequest{
+				// Length 128 chars.
+				// If AutoTune=true (block size 64): 2 blocks
+				// If AutoTune=false (block size 32): 4 blocks
+				Prompt: strings.Repeat("a", 128),
+			},
+		},
+	}
+
+	t.Run("AutoTune Enabled", func(t *testing.T) {
+		config := Config{
+			AutoTune:               true,
+			BlockSize:              32, // Should be ignored in favor of pod metrics (64)
+			MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
+			// Should be ignored in favor of pod metrics (1000)
+			LRUCapacityPerServer: 1,
+		}
+		plugin := New(context.Background(), config)
+
+		// 1. Verify Score uses pod metrics for block size
+		scores := plugin.Score(context.Background(), types.NewCycleState(), req, pods)
+		_ = scores
+
+		state, err := plugins.ReadPluginStateKey[*SchedulingContextState](plugin.pluginState, req.RequestId, plugins.StateKey(plugin.TypedName().String()))
+		assert.NoError(t, err)
+		// Block size from pod is 16 tokens * 4 = 64 chars.
+		// Prompt is 128 chars.
+		// Expected blocks: 128/64 = 2 hashes (model hash is used as seed but not returned as a block)
+		assert.Equal(t, 2, len(state.PrefixHashes), "Should use pod block size (64 chars) -> 2 body blocks")
+
+		// 2. Verify PreRequest uses pod metrics for capacity
+		schedulingResult := &types.SchedulingResult{
+			PrimaryProfileName: "default",
+			ProfileResults: map[string]*types.ProfileRunResult{
+				"default": {TargetPods: []types.Pod{pod}},
+			},
+		}
+		plugin.PreRequest(context.Background(), req, schedulingResult)
+		plugin.wg.Wait()
+
+		// Check indexer state
+		assert.Contains(t, plugin.indexer.Pods(), ServerID(pod.GetPod().NamespacedName))
+	})
+
+	t.Run("AutoTune Disabled", func(t *testing.T) {
+		config := Config{
+			AutoTune:               false,
+			BlockSize:              32, // Should be used (32 chars)
+			MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
+			LRUCapacityPerServer:   1, // Should be used, and the first hash should be evicted due to the small
+		}
+		plugin := New(context.Background(), config)
+
+		// 1. Verify Score uses config BlockSize
+		req.RequestId = uuid.NewString() // New request ID
+		scores := plugin.Score(context.Background(), types.NewCycleState(), req, pods)
+		_ = scores
+
+		state, err := plugins.ReadPluginStateKey[*SchedulingContextState](plugin.pluginState, req.RequestId, plugins.StateKey(plugin.TypedName().String()))
+		assert.NoError(t, err)
+		// Block size from config is 32 chars.
+		// Prompt is 128 chars.
+		// 128 / 32 = 4 chunks.
+		assert.Equal(t, 4, len(state.PrefixHashes), "Should use config block size (32 chars) -> 4 body blocks")
+
+		// 2. Verify PreRequest uses config LRUCapacityPerServer
+		schedulingResult := &types.SchedulingResult{
+			PrimaryProfileName: "default",
+			ProfileResults: map[string]*types.ProfileRunResult{
+				"default": {TargetPods: []types.Pod{pod}},
+			},
+		}
+		plugin.PreRequest(context.Background(), req, schedulingResult)
+		plugin.wg.Wait()
+
+		assert.Contains(t, plugin.indexer.Pods(), ServerID(pod.GetPod().NamespacedName))
+	})
+}
+
 // randomPrompt generates a pseudo-random string of length n using lowercase letters.
 func randomPrompt(n int) string {
 	runes := []rune("abcdefghijklmnopqrstuvwxyz")
@@ -481,7 +576,7 @@ func BenchmarkPrefixPluginChatCompletionsStress(b *testing.B) {
 	blockSize := 8
 	maxPrefixBlocks := 50000
 	config := Config{
-		DefaultBlockSize:       blockSize,
+		BlockSize:              blockSize,
 		MaxPrefixBlocksToMatch: maxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/latencypredictor_helper_test.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/latencypredictor_helper_test.go
new file mode 100644
index 0000000..92227cb
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/latencypredictor_helper_test.go
@@ -0,0 +1,100 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	latencypredictor "sigs.k8s.io/gateway-api-inference-extension/sidecars/latencypredictorasync"
+)
+
+func TestBulkPredictWithMetrics(t *testing.T) {
+	mockPredictor := &mockPredictor{
+		predictions: map[string]*latencypredictor.PredictionResponse{
+			"0.5": {TTFT: 0.5, TPOT: 0.03},
+			"0.6": {TTFT: 0.6, TPOT: 0.04},
+		},
+	}
+
+	metricsStates := []*backendmetrics.MetricsState{
+		{KVCacheUsagePercent: 0.5},
+		{KVCacheUsagePercent: 0.6},
+	}
+	prompts := []string{"prompt1", "prompt2"}
+	generatedTokenCounts := []int{1, 1}
+	prefixCacheScores := []float64{0.0, 0.0}
+
+	results, err := bulkPredictWithMetrics(context.Background(), mockPredictor, metricsStates, prompts, generatedTokenCounts, prefixCacheScores)
+
+	assert.NoError(t, err)
+	assert.Len(t, results, 2)
+	assert.Equal(t, 0.5, results[0].TTFT)
+	assert.Equal(t, 0.03, results[0].TPOT)
+	assert.Equal(t, 0.6, results[1].TTFT)
+	assert.Equal(t, 0.04, results[1].TPOT)
+}
+
+func TestBulkPredictWithMetrics_Error(t *testing.T) {
+	mockPredictor := &mockPredictor{
+		err: errors.New("prediction failed"),
+	}
+
+	metricsStates := []*backendmetrics.MetricsState{
+		{KVCacheUsagePercent: 0.5},
+	}
+	prompts := []string{"prompt1"}
+	generatedTokenCounts := []int{1}
+	prefixCacheScores := []float64{0.0}
+
+	results, err := bulkPredictWithMetrics(context.Background(), mockPredictor, metricsStates, prompts, generatedTokenCounts, prefixCacheScores)
+
+	assert.Error(t, err)
+	assert.Nil(t, results)
+}
+
+func TestBulkPredictWithMetrics_InputMismatch(t *testing.T) {
+	mockPredictor := &mockPredictor{}
+	metricsStates := []*backendmetrics.MetricsState{{}}
+	prompts := []string{"prompt1", "prompt2"} // Mismatch length
+	generatedTokenCounts := []int{1}
+	prefixCacheScores := []float64{0.0}
+
+	results, err := bulkPredictWithMetrics(context.Background(), mockPredictor, metricsStates, prompts, generatedTokenCounts, prefixCacheScores)
+
+	assert.Error(t, err)
+	assert.Nil(t, results)
+	assert.True(t, strings.Contains(err.Error(), "input slice lengths must match"))
+}
+
+func TestBulkPredictWithMetrics_NilMetricsState(t *testing.T) {
+	mockPredictor := &mockPredictor{}
+	metricsStates := []*backendmetrics.MetricsState{nil} // Nil metrics state
+	prompts := []string{"prompt1"}
+	generatedTokenCounts := []int{1}
+	prefixCacheScores := []float64{0.0}
+
+	results, err := bulkPredictWithMetrics(context.Background(), mockPredictor, metricsStates, prompts, generatedTokenCounts, prefixCacheScores)
+
+	assert.Error(t, err)
+	assert.Nil(t, results)
+	assert.True(t, strings.Contains(err.Error(), "metrics state at index 0 cannot be nil"))
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/requestcontrol_hooks_test.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/requestcontrol_hooks_test.go
new file mode 100644
index 0000000..5aaf1a2
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/requestcontrol_hooks_test.go
@@ -0,0 +1,952 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"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/requestcontrol"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
+)
+
+const (
+	testModelName = "test-model"
+	kvUsage       = 1
+	runningQueue  = 1
+	waitingQueue  = 1
+)
+
+// Helper functions
+
+func createTestSchedulingResult(pod *backend.Pod) *schedulingtypes.SchedulingResult {
+
+	mockPod := createTestPod(pod.NamespacedName.Name, kvUsage, runningQueue, waitingQueue)
+
+	return &schedulingtypes.SchedulingResult{
+		PrimaryProfileName: "default",
+		ProfileResults: map[string]*schedulingtypes.ProfileRunResult{
+			"default": {
+				TargetPods: []schedulingtypes.Pod{mockPod},
+			},
+		},
+	}
+}
+
+func createTestRouter() *SLOAwareRouter {
+	return &SLOAwareRouter{
+		sloContextStore:     sync.Map{},
+		runningRequestLists: make(map[types.NamespacedName]*requestPriorityQueue),
+		latencypredictor:    nil,
+	}
+}
+
+// Test cases
+
+func TestNewSLORequestContext(t *testing.T) {
+	request := createTestLLMRequest("test", 100, 50, true)
+
+	ctx := newSLORequestContext(request)
+
+	assert.NotNil(t, ctx)
+	assert.Equal(t, *request, ctx.schedulingRequest)
+	assert.NotNil(t, ctx.lastSeenMetrics)
+	assert.NotNil(t, ctx.prefixCacheScoresForPods)
+	assert.NotNil(t, ctx.predictedTTFTForScheduling)
+	assert.NotNil(t, ctx.predictedTPOTForScheduling)
+	assert.Empty(t, ctx.lastSeenMetrics)
+	assert.Empty(t, ctx.prefixCacheScoresForPods)
+}
+
+func TestSLOAwareRouter_SetAndGetSLOContext(t *testing.T) {
+	router := createTestRouter()
+	request := createTestLLMRequest("test", 100, 50, true)
+	sloCtx := newSLORequestContext(request)
+
+	// Set context
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Get context
+	retrievedCtx, err := router.getSLOContextForRequest(request)
+
+	require.NoError(t, err)
+	assert.Equal(t, sloCtx, retrievedCtx)
+}
+
+func TestSLOAwareRouter_GetSLOContext_NotFound(t *testing.T) {
+	router := createTestRouter()
+	request := createTestLLMRequest("test", 100, 50, true)
+
+	// Try to get context that doesn't exist
+	ctx, err := router.getSLOContextForRequest(request)
+
+	assert.Error(t, err)
+	assert.Nil(t, ctx)
+	assert.Contains(t, err.Error(), "SLO context not found")
+}
+
+func TestSLOAwareRouter_DeleteSLOContext(t *testing.T) {
+	router := createTestRouter()
+	request := createTestLLMRequest("test", 100, 50, true)
+	sloCtx := newSLORequestContext(request)
+
+	// Set and then delete context
+	router.setSLOContextForRequest(request, sloCtx)
+	router.deleteSLOContextForRequest(request)
+
+	// Verify it's deleted
+	ctx, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+	assert.Nil(t, ctx)
+}
+
+func TestSLOAwareRouter_PreRequest_NoSchedulingResult(t *testing.T) {
+	router := createTestRouter()
+	ctx := context.Background()
+	request := createTestLLMRequest("test", 100, 50, true)
+
+	// Call PreRequest with nil scheduling result
+	router.PreRequest(ctx, request, nil)
+
+	// Should not create SLO context
+	_, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+}
+
+func TestSLOAwareRouter_PreRequest_EmptySchedulingResult(t *testing.T) {
+	router := createTestRouter()
+	ctx := context.Background()
+	request := createTestLLMRequest("test", 100, 50, true)
+
+	schedulingResult := &schedulingtypes.SchedulingResult{
+		ProfileResults: map[string]*schedulingtypes.ProfileRunResult{},
+	}
+
+	// Call PreRequest with empty scheduling result
+	router.PreRequest(ctx, request, schedulingResult)
+
+	// Should not create SLO context
+	_, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+}
+
+func TestSLOAwareRouter_PreRequest_Success(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	// Create and set initial SLO context
+	sloCtx := newSLORequestContext(request)
+	sloCtx.avgTPOTSLO = 50
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Initialize the request priority queue
+	router.runningRequestLists[pod.GetPod().NamespacedName] = newRequestPriorityQueue()
+
+	beforeTime := time.Now()
+	router.PreRequest(ctx, request, schedulingResult)
+	afterTime := time.Now()
+
+	// Verify SLO context was updated
+	retrievedCtx, err := router.getSLOContextForRequest(request)
+	require.NoError(t, err)
+	assert.Equal(t, pod.GetPod(), retrievedCtx.targetPod)
+	assert.Equal(t, schedulingResult, retrievedCtx.schedulingResult)
+	assert.True(t, retrievedCtx.requestReceivedTimestamp.After(beforeTime) ||
+		retrievedCtx.requestReceivedTimestamp.Equal(beforeTime))
+	assert.True(t, retrievedCtx.requestReceivedTimestamp.Before(afterTime) ||
+		retrievedCtx.requestReceivedTimestamp.Equal(afterTime))
+}
+
+func TestSLOAwareRouter_PreRequest_AddsToQueue(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	// Create and set initial SLO context
+	sloCtx := newSLORequestContext(request)
+	sloCtx.avgTPOTSLO = 50
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// PreRequest should create the queue
+	router.PreRequest(ctx, request, schedulingResult)
+
+	// Verify queue was created and request was added
+	queue, exists := router.runningRequestLists[pod.GetPod().NamespacedName]
+	assert.True(t, exists, "Queue should be created for pod")
+	assert.NotNil(t, queue)
+}
+
+func TestSLOAwareRouter_PreRequest_QueueAlreadyExists(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request1 := createTestLLMRequest("test-id-1", 100, 50, true)
+	request2 := createTestLLMRequest("test-id-2", 100, 50, true)
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	// Create and set initial SLO contexts
+	sloCtx1 := newSLORequestContext(request1)
+	sloCtx1.avgTPOTSLO = 50
+	router.setSLOContextForRequest(request1, sloCtx1)
+
+	sloCtx2 := newSLORequestContext(request2)
+	sloCtx2.avgTPOTSLO = 50
+	router.setSLOContextForRequest(request2, sloCtx2)
+
+	// Add first request
+	router.PreRequest(ctx, request1, schedulingResult)
+
+	// Add second request to same pod
+	router.PreRequest(ctx, request2, schedulingResult)
+
+	// Verify both are in the same queue
+	queue, exists := router.runningRequestLists[pod.GetPod().NamespacedName]
+	assert.True(t, exists)
+	assert.NotNil(t, queue)
+}
+
+func TestSLOAwareRouter_ResponseReceived_NilPredictor(t *testing.T) {
+	router := createTestRouter()
+	router.latencypredictor = nil
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	sloCtx := newSLORequestContext(request)
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should not panic and should return early
+	router.ResponseReceived(ctx, request, response, pod.GetPod())
+
+	// Context should still exist
+	_, err := router.getSLOContextForRequest(request)
+	assert.NoError(t, err)
+}
+
+func TestSLOAwareRouter_ResponseReceived_NoPod(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	sloCtx := newSLORequestContext(request)
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should not panic with nil pod
+	router.ResponseReceived(ctx, request, response, nil)
+
+	// Predictor should not be called
+
+}
+
+func TestSLOAwareRouter_ResponseReceived_NoContext(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	// Don't set SLO context
+	router.ResponseReceived(ctx, request, response, pod.GetPod())
+
+	// Should handle missing context gracefully
+
+}
+
+func TestSLOAwareRouter_ResponseStreaming_NilPredictor(t *testing.T) {
+	router := createTestRouter()
+	router.latencypredictor = nil
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	sloCtx := newSLORequestContext(request)
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should not panic and should return early
+	router.ResponseStreaming(ctx, request, response, pod.GetPod())
+
+	// Context should still exist
+	_, err := router.getSLOContextForRequest(request)
+	assert.NoError(t, err)
+}
+func TestSLOAwareRouter_ResponseStreaming_FirstToken(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	sloCtx := newSLORequestContext(request)
+	sloCtx.requestReceivedTimestamp = time.Now()
+	sloCtx.schedulingResult = schedulingResult
+	sloCtx.schedulingRequest = *request
+	sloCtx.ttftSLO = 100
+	sloCtx.avgTPOTSLO = 50
+	sloCtx.incomingModelName = testModelName
+	sloCtx.predictedTTFT = 80.0
+	sloCtx.avgPredictedTPOT = 30.0
+	// ADD THIS - populate metrics
+	sloCtx.lastSeenMetrics["prefill"] = &backendmetrics.MetricsState{
+		KVCacheUsagePercent: 0.5,
+		WaitingQueueSize:    1,
+		RunningQueueSize:    1,
+	}
+	sloCtx.lastSeenMetrics["default"] = &backendmetrics.MetricsState{
+		KVCacheUsagePercent: 0.5,
+		WaitingQueueSize:    1,
+		RunningQueueSize:    1,
+	}
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Initialize the queue and add the request
+	queue := newRequestPriorityQueue()
+	queue.Add(request.Headers[requtil.RequestIdHeaderKey], 50.0)
+	router.runningRequestLists[pod.GetPod().NamespacedName] = queue
+
+	beforeTime := time.Now()
+	router.ResponseStreaming(ctx, request, response, pod.GetPod())
+	afterTime := time.Now()
+
+	// Verify first token timestamp was set
+	retrievedCtx, err := router.getSLOContextForRequest(request)
+	require.NoError(t, err)
+	assert.True(t, retrievedCtx.lastTokenTimestamp.After(beforeTime) ||
+		retrievedCtx.lastTokenTimestamp.Equal(beforeTime))
+	assert.True(t, retrievedCtx.lastTokenTimestamp.Before(afterTime) ||
+		retrievedCtx.lastTokenTimestamp.Equal(afterTime))
+}
+
+func TestSLOAwareRouter_ResponseStreaming_SubsequentTokens(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	sloCtx := newSLORequestContext(request)
+	sloCtx.requestReceivedTimestamp = time.Now()
+	sloCtx.schedulingResult = schedulingResult
+	sloCtx.schedulingRequest = *request
+	sloCtx.ttftSLO = 100
+	sloCtx.avgTPOTSLO = 50
+	sloCtx.incomingModelName = testModelName
+	sloCtx.predictedTTFT = 80.0
+	sloCtx.avgPredictedTPOT = 30.0
+	// ADD THIS - populate metrics
+	sloCtx.lastSeenMetrics["prefill"] = &backendmetrics.MetricsState{
+		KVCacheUsagePercent: 0.5,
+		WaitingQueueSize:    1,
+		RunningQueueSize:    1,
+	}
+	sloCtx.lastSeenMetrics["default"] = &backendmetrics.MetricsState{
+		KVCacheUsagePercent: 0.5,
+		WaitingQueueSize:    1,
+		RunningQueueSize:    1,
+	}
+	firstTokenTime := time.Now().Add(-100 * time.Millisecond)
+
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Initialize the queue and add the request
+	queue := newRequestPriorityQueue()
+	queue.Add(request.Headers[requtil.RequestIdHeaderKey], 50.0)
+	router.runningRequestLists[pod.GetPod().NamespacedName] = queue
+
+	router.ResponseStreaming(ctx, request, response, pod.GetPod())
+
+	// Verify token timestamp was updated
+	retrievedCtx, err := router.getSLOContextForRequest(request)
+	require.NoError(t, err)
+	assert.True(t, retrievedCtx.lastTokenTimestamp.After(firstTokenTime))
+}
+
+func TestSLOAwareRouter_ResponseComplete_QueueNotFound(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	sloCtx := newSLORequestContext(request)
+	sloCtx.incomingModelName = testModelName
+	sloCtx.targetPod = pod.GetPod() // ADD THIS to avoid other issues
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Create an EMPTY queue (not nil, but empty) to test queue.Remove behavior
+	router.runningRequestLists[pod.GetPod().NamespacedName] = newRequestPriorityQueue()
+
+	// Should handle gracefully when request is not in queue
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Context should be deleted
+	_, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+}
+func TestSLOAwareRouter_ResponseStreaming_NoContext(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	// Don't set SLO context - should handle gracefully
+	router.ResponseStreaming(ctx, request, response, pod.GetPod())
+
+	// Should not panic
+
+}
+
+func TestSLOAwareRouter_ResponseComplete_Success(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	// Create queue and add request
+	queue := newRequestPriorityQueue()
+	router.runningRequestLists[pod.GetPod().NamespacedName] = queue
+	queue.Add(request.Headers[requtil.RequestIdHeaderKey], 50.0)
+
+	sloCtx := newSLORequestContext(request)
+	sloCtx.ttft = 80
+	sloCtx.avgTPOT = 30
+	sloCtx.predictedTTFT = 85
+	sloCtx.avgPredictedTPOT = 32
+	sloCtx.ttftSLO = 100
+	sloCtx.avgTPOTSLO = 50
+	sloCtx.incomingModelName = "incoming-model"
+	router.setSLOContextForRequest(request, sloCtx)
+
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Verify context was deleted
+	_, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+
+	// Verify request was removed from queue
+	assert.Equal(t, 0, queue.Len())
+}
+
+func TestSLOAwareRouter_ResponseComplete_NilPredictor(t *testing.T) {
+	router := createTestRouter()
+	router.latencypredictor = nil
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	sloCtx := newSLORequestContext(request)
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should not panic
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Context should still exist (deletion happens only with predictor)
+	_, err := router.getSLOContextForRequest(request)
+	assert.NoError(t, err)
+}
+
+func TestSLOAwareRouter_ResponseComplete_NoPod(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	sloCtx := newSLORequestContext(request)
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should not panic with nil pod
+	router.ResponseComplete(ctx, request, response, nil)
+
+	// Context should still exist (deletion happens only with validpod.GetPod())
+	_, err := router.getSLOContextForRequest(request)
+	assert.NoError(t, err)
+}
+
+func TestSLOAwareRouter_ResponseComplete_NoContext(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	// Don't set SLO context - should handle gracefully
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Should not panic
+
+}
+
+func TestSLOAwareRouter_ResponseComplete_WithMetrics(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+
+	// Create queue
+	queue := newRequestPriorityQueue()
+	router.runningRequestLists[pod.GetPod().NamespacedName] = queue
+	queue.Add(request.Headers[requtil.RequestIdHeaderKey], 50.0)
+
+	sloCtx := newSLORequestContext(request)
+	sloCtx.ttft = 80
+	sloCtx.avgTPOT = 30
+	sloCtx.predictedTTFT = 85
+	sloCtx.avgPredictedTPOT = 32
+	sloCtx.ttftSLO = 100
+	sloCtx.avgTPOTSLO = 50
+	sloCtx.incomingModelName = "incoming-model"
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should record metrics without panicking
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Verify cleanup
+	_, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+}
+
+func TestSLOAwareRouter_ResponseComplete_NoSLOs(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test-id", 0, 0, true) // No SLOs
+	response := &requestcontrol.Response{}
+
+	// Create queue
+	queue := newRequestPriorityQueue()
+	router.runningRequestLists[pod.GetPod().NamespacedName] = queue
+	queue.Add(request.Headers[requtil.RequestIdHeaderKey], 0)
+
+	sloCtx := newSLORequestContext(request)
+	sloCtx.ttft = 80
+	sloCtx.avgTPOT = 30
+	sloCtx.incomingModelName = testModelName
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// Should handle missing SLOs gracefully
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Verify cleanup
+	_, err := router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+}
+
+func TestSLOAwareRouter_CheckPredictor_NilPod(t *testing.T) {
+	router := createTestRouter()
+	logger := logr.Discard()
+
+	result := router.checkPredictor(logger, nil)
+
+	assert.False(t, result)
+}
+
+func TestSLOAwareRouter_CheckPredictor_NilPredictor(t *testing.T) {
+	router := createTestRouter()
+	router.latencypredictor = nil
+	logger := logr.Discard()
+	pod := createTestPod("test-pod", 1, 1, 1)
+
+	result := router.checkPredictor(logger, pod.GetPod())
+
+	assert.False(t, result)
+}
+
+func TestSLOAwareRouter_CheckPredictor_Success(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+	logger := logr.Discard()
+	pod := createTestPod("test-pod", 1, 1, 1)
+
+	result := router.checkPredictor(logger, pod.GetPod())
+
+	assert.True(t, result)
+}
+
+func TestSLORequestContext_Fields(t *testing.T) {
+	request := createTestLLMRequest("test", 100, 50, true)
+	ctx := newSLORequestContext(request)
+
+	// Test all field initialization
+	assert.NotNil(t, ctx.lastSeenMetrics)
+	assert.NotNil(t, ctx.prefixCacheScoresForPods)
+	assert.NotNil(t, ctx.predictedTTFTForScheduling)
+	assert.NotNil(t, ctx.predictedTPOTForScheduling)
+	assert.Empty(t, ctx.tpotObservations)
+	assert.Empty(t, ctx.predictedTPOTObservations)
+	assert.Zero(t, ctx.generatedTokenCount)
+	assert.Zero(t, ctx.ttft)
+	assert.Zero(t, ctx.avgTPOT)
+	assert.Nil(t, ctx.targetPod)
+	assert.Nil(t, ctx.schedulingResult)
+	assert.Nil(t, ctx.tokenSampler)
+}
+
+func TestSLORequestContext_UpdateMetrics(t *testing.T) {
+	request := createTestLLMRequest("test", 100, 50, true)
+	ctx := newSLORequestContext(request)
+
+	// Add some metrics
+	metricsState := &backendmetrics.MetricsState{
+		KVCacheUsagePercent: 0.5,
+		WaitingQueueSize:    3,
+	}
+	ctx.lastSeenMetrics["test-pod"] = metricsState
+
+	assert.Len(t, ctx.lastSeenMetrics, 1)
+	assert.Equal(t, 0.5, ctx.lastSeenMetrics["test-pod"].KVCacheUsagePercent)
+	assert.Equal(t, 3, ctx.lastSeenMetrics["test-pod"].WaitingQueueSize)
+}
+
+func TestSLORequestContext_PredictionData(t *testing.T) {
+	request := createTestLLMRequest("test", 100, 50, true)
+	ctx := newSLORequestContext(request)
+
+	// Set prediction data
+	ctx.predictedTTFTForScheduling["pod1"] = 80.0
+	ctx.predictedTPOTForScheduling["pod1"] = 30.0
+	ctx.predictedTTFTForScheduling["pod2"] = 90.0
+	ctx.predictedTPOTForScheduling["pod2"] = 35.0
+
+	assert.Len(t, ctx.predictedTTFTForScheduling, 2)
+	assert.Len(t, ctx.predictedTPOTForScheduling, 2)
+	assert.Equal(t, 80.0, ctx.predictedTTFTForScheduling["pod1"])
+	assert.Equal(t, 30.0, ctx.predictedTPOTForScheduling["pod1"])
+}
+
+func TestSLORequestContext_PrefixCacheScores(t *testing.T) {
+	request := createTestLLMRequest("test", 100, 50, true)
+	ctx := newSLORequestContext(request)
+
+	// Set prefix cache scores
+	ctx.prefixCacheScoresForPods["pod1"] = 0.8
+	ctx.prefixCacheScoresForPods["pod2"] = 0.6
+	ctx.prefixCacheScoresForPods["pod3"] = 0.9
+
+	assert.Len(t, ctx.prefixCacheScoresForPods, 3)
+	assert.Equal(t, 0.8, ctx.prefixCacheScoresForPods["pod1"])
+	assert.Equal(t, 0.9, ctx.prefixCacheScoresForPods["pod3"])
+}
+
+func TestSLOAwareRouter_ConcurrentContextAccess(t *testing.T) {
+	router := createTestRouter()
+
+	// Test concurrent access to context store
+	var wg sync.WaitGroup
+	numGoroutines := 100
+
+	for i := 0; i < numGoroutines; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+
+			requestID := uuid.New().String()
+			request := createTestLLMRequest(requestID, 100, 50, true)
+			sloCtx := newSLORequestContext(request)
+
+			// Set context
+			router.setSLOContextForRequest(request, sloCtx)
+
+			// Get context
+			retrievedCtx, err := router.getSLOContextForRequest(request)
+			assert.NoError(t, err)
+			assert.NotNil(t, retrievedCtx)
+
+			// Delete context
+			router.deleteSLOContextForRequest(request)
+		}()
+	}
+
+	wg.Wait()
+}
+
+func TestSLOAwareRouter_MultipleRequests_SamePod(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+
+	request1 := createTestLLMRequest("test-id-1", 100, 50, true)
+	request2 := createTestLLMRequest("test-id-2", 100, 50, true)
+	request3 := createTestLLMRequest("test-id-3", 100, 50, true)
+
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	// Create and set SLO contexts
+	for _, req := range []*schedulingtypes.LLMRequest{request1, request2, request3} {
+		sloCtx := newSLORequestContext(req)
+		sloCtx.avgTPOTSLO = 50
+		router.setSLOContextForRequest(req, sloCtx)
+	}
+
+	// Add all requests
+	router.PreRequest(ctx, request1, schedulingResult)
+	router.PreRequest(ctx, request2, schedulingResult)
+	router.PreRequest(ctx, request3, schedulingResult)
+
+	// Verify queue has all requests
+	queue, exists := router.runningRequestLists[pod.GetPod().NamespacedName]
+	assert.True(t, exists)
+	assert.NotNil(t, queue)
+}
+
+func TestSLOAwareRouter_RequestLifecycle_Complete(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	request := createTestLLMRequest("test", 100, 50, true)
+	response := &requestcontrol.Response{}
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	// Create initial context
+	sloCtx := newSLORequestContext(request)
+	sloCtx.avgTPOTSLO = 50
+	sloCtx.incomingModelName = testModelName
+	router.setSLOContextForRequest(request, sloCtx)
+
+	// 1. PreRequest
+	router.PreRequest(ctx, request, schedulingResult)
+
+	// Verify context exists
+	retrievedCtx, err := router.getSLOContextForRequest(request)
+	require.NoError(t, err)
+	assert.NotNil(t, retrievedCtx.targetPod)
+
+	// 2. ResponseReceived
+	router.ResponseReceived(ctx, request, response, pod.GetPod())
+
+	// 3. ResponseStreaming (first token)
+	router.ResponseStreaming(ctx, request, response, pod.GetPod())
+
+	// 4. ResponseStreaming (subsequent tokens)
+	retrievedCtx, _ = router.getSLOContextForRequest(request)
+	retrievedCtx.ttft = 100 // Mark first token received
+	router.setSLOContextForRequest(request, retrievedCtx)
+	router.ResponseStreaming(ctx, request, response, pod.GetPod())
+
+	// 5. ResponseComplete
+	retrievedCtx, _ = router.getSLOContextForRequest(request)
+	retrievedCtx.ttft = 80
+	retrievedCtx.avgTPOT = 30
+	router.setSLOContextForRequest(request, retrievedCtx)
+	router.ResponseComplete(ctx, request, response, pod.GetPod())
+
+	// Verify context was cleaned up
+	_, err = router.getSLOContextForRequest(request)
+	assert.Error(t, err)
+}
+
+func TestSLOAwareRouter_MultipleRequests_DifferentPods(t *testing.T) {
+	router := createTestRouter()
+	mockPredictor := new(mockPredictor)
+	router.latencypredictor = mockPredictor
+
+	ctx := context.Background()
+
+	pod1 := createTestPod("test-pod-1", 1, 1, 1)
+	pod2 := createTestPod("test-pod-2", 1, 1, 1)
+
+	request1 := createTestLLMRequest("test-id-1", 100, 50, true)
+	request2 := createTestLLMRequest("test-id-2", 100, 50, true)
+
+	schedulingResult1 := createTestSchedulingResult(pod1.GetPod())
+	schedulingResult2 := createTestSchedulingResult(pod2.GetPod())
+
+	// Create and set SLO contexts
+	sloCtx1 := newSLORequestContext(request1)
+	sloCtx1.avgTPOTSLO = 50
+	router.setSLOContextForRequest(request1, sloCtx1)
+
+	sloCtx2 := newSLORequestContext(request2)
+	sloCtx2.avgTPOTSLO = 50
+	router.setSLOContextForRequest(request2, sloCtx2)
+
+	// Add requests to different pods
+	router.PreRequest(ctx, request1, schedulingResult1)
+	router.PreRequest(ctx, request2, schedulingResult2)
+
+	// Verify separate queues were created
+	queue1, exists1 := router.runningRequestLists[pod1.GetPod().NamespacedName]
+	queue2, exists2 := router.runningRequestLists[pod2.GetPod().NamespacedName]
+
+	assert.True(t, exists1)
+	assert.True(t, exists2)
+	assert.NotNil(t, queue1)
+	assert.NotNil(t, queue2)
+	assert.NotEqual(t, queue1, queue2)
+}
+
+func TestSLORequestContext_SLOValidation(t *testing.T) {
+	tests := []struct {
+		name       string
+		ttftSLO    float64
+		tpotSLO    float64
+		expectSLOs bool
+	}{
+		{
+			name:       "Both SLOs set",
+			ttftSLO:    100,
+			tpotSLO:    50,
+			expectSLOs: true,
+		},
+		{
+			name:       "No SLOs",
+			ttftSLO:    0,
+			tpotSLO:    0,
+			expectSLOs: false,
+		},
+		{
+			name:       "Only TTFT SLO",
+			ttftSLO:    100,
+			tpotSLO:    0,
+			expectSLOs: false,
+		},
+		{
+			name:       "Only TPOT SLO",
+			ttftSLO:    0,
+			tpotSLO:    50,
+			expectSLOs: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			request := createTestLLMRequest("test-id", tt.ttftSLO, tt.tpotSLO, true)
+			ctx := newSLORequestContext(request)
+			ctx.ttftSLO = tt.ttftSLO
+			ctx.avgTPOTSLO = tt.tpotSLO
+
+			hasBothSLOs := ctx.ttftSLO > 0 && ctx.avgTPOTSLO > 0
+			assert.Equal(t, tt.expectSLOs, hasBothSLOs)
+		})
+	}
+}
+
+// Benchmark tests
+
+func BenchmarkSLOAwareRouter_PreRequest(b *testing.B) {
+	router := createTestRouter()
+	ctx := context.Background()
+	pod := createTestPod("test-pod", 1, 1, 1)
+	schedulingResult := createTestSchedulingResult(pod.GetPod())
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		requestID := uuid.New().String()
+		request := createTestLLMRequest(requestID, 100, 50, true)
+		sloCtx := newSLORequestContext(request)
+		sloCtx.avgTPOTSLO = 50
+		router.setSLOContextForRequest(request, sloCtx)
+		router.PreRequest(ctx, request, schedulingResult)
+	}
+}
+
+func BenchmarkSLOAwareRouter_ContextOperations(b *testing.B) {
+	router := createTestRouter()
+	request := createTestLLMRequest("test", 100, 50, true)
+	sloCtx := newSLORequestContext(request)
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		router.setSLOContextForRequest(request, sloCtx)
+		_, _ = router.getSLOContextForRequest(request)
+		router.deleteSLOContextForRequest(request)
+	}
+}
+
+func BenchmarkSLORequestContext_Creation(b *testing.B) {
+	request := createTestLLMRequest("test", 100, 50, true)
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_ = newSLORequestContext(request)
+	}
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/running_request_queue_test.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/running_request_queue_test.go
new file mode 100644
index 0000000..ef34c84
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/running_request_queue_test.go
@@ -0,0 +1,391 @@
+/*
+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 slo_aware_router
+
+import (
+	"fmt"
+	"sync"
+	"testing"
+	"time"
+)
+
+func TestNewRequestPriorityQueue(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	if pq == nil {
+		t.Fatal("NewRequestPriorityQueue returned nil")
+	}
+
+	if pq.GetSize() != 0 {
+		t.Errorf("Expected empty queue, got size %d", pq.GetSize())
+	}
+
+	if pq.Peek() != nil {
+		t.Error("Expected nil from Peek on empty queue")
+	}
+}
+
+func TestAdd(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Test successful add
+	if !pq.Add("req1", 2.5) {
+		t.Error("Expected Add to return true for new item")
+	}
+
+	if pq.GetSize() != 1 {
+		t.Errorf("Expected size 1, got %d", pq.GetSize())
+	}
+
+	// Test duplicate add
+	if pq.Add("req1", 3.0) {
+		t.Error("Expected Add to return false for duplicate ID")
+	}
+
+	if pq.GetSize() != 1 {
+		t.Errorf("Expected size 1 after duplicate add, got %d", pq.GetSize())
+	}
+
+	// Test validation
+	if pq.Add("", 1.0) {
+		t.Error("Expected Add to return false for empty ID")
+	}
+
+	if pq.Add("req2", -1.0) {
+		t.Error("Expected Add to return false for negative TPOT")
+	}
+}
+
+func TestPriorityOrdering(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Add items with different priorities
+	pq.Add("high", 1.0)   // highest priority (lowest TPOT)
+	pq.Add("medium", 5.0) // medium priority
+	pq.Add("low", 10.0)   // lowest priority (highest TPOT)
+
+	// Check that highest priority item is at the top
+	peek := pq.Peek()
+	if peek == nil || peek.id != "high" || peek.tpot != 1.0 {
+		t.Errorf("Expected high priority item at top, got %+v", peek)
+	}
+
+	// Test removal order
+	expected := []struct {
+		id   string
+		tpot float64
+	}{
+		{"high", 1.0},
+		{"medium", 5.0},
+		{"low", 10.0},
+	}
+
+	for _, exp := range expected {
+		item := pq.Peek()
+		if item.id != exp.id || item.tpot != exp.tpot {
+			t.Errorf("Expected %s(%.1f), got %s(%.1f)", exp.id, exp.tpot, item.id, item.tpot)
+		}
+
+		removed, ok := pq.Remove(item.id)
+		if !ok || removed.id != exp.id {
+			t.Errorf("Failed to remove %s", exp.id)
+		}
+	}
+}
+
+func TestRemove(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Test remove from empty queue
+	if _, ok := pq.Remove("nonexistent"); ok {
+		t.Error("Expected Remove to return false for empty queue")
+	}
+
+	// Add some items
+	pq.Add("req1", 1.0)
+	pq.Add("req2", 2.0)
+	pq.Add("req3", 3.0)
+
+	// Test successful remove
+	removed, ok := pq.Remove("req2")
+	if !ok || removed.id != "req2" || removed.tpot != 2.0 {
+		t.Errorf("Expected to remove req2(2.0), got %+v, ok=%v", removed, ok)
+	}
+
+	if pq.GetSize() != 2 {
+		t.Errorf("Expected size 2 after removal, got %d", pq.GetSize())
+	}
+
+	// Test remove nonexistent
+	if _, ok := pq.Remove("req2"); ok {
+		t.Error("Expected Remove to return false for already removed item")
+	}
+
+	// Verify remaining items are still in correct order
+	if peek := pq.Peek(); peek.id != "req1" {
+		t.Errorf("Expected req1 at top, got %s", peek.id)
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Test update nonexistent item
+	if pq.Update("nonexistent", 1.0) {
+		t.Error("Expected Update to return false for nonexistent item")
+	}
+
+	// Add items
+	pq.Add("req1", 1.0)
+	pq.Add("req2", 2.0)
+	pq.Add("req3", 3.0)
+
+	// Update to make req3 highest priority
+	if !pq.Update("req3", 0.5) {
+		t.Error("Expected Update to return true for existing item")
+	}
+
+	// Check that req3 is now at the top
+	if peek := pq.Peek(); peek.id != "req3" || peek.tpot != 0.5 {
+		t.Errorf("Expected req3(0.5) at top, got %s(%.1f)", peek.id, peek.tpot)
+	}
+
+	// Test validation
+	if pq.Update("req1", -1.0) {
+		t.Error("Expected Update to return false for negative TPOT")
+	}
+}
+
+func TestContains(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Test empty queue
+	if pq.Contains("req1") {
+		t.Error("Expected Contains to return false for empty queue")
+	}
+
+	// Add item
+	pq.Add("req1", 1.0)
+
+	// Test existing item
+	if !pq.Contains("req1") {
+		t.Error("Expected Contains to return true for existing item")
+	}
+
+	// Test nonexistent item
+	if pq.Contains("req2") {
+		t.Error("Expected Contains to return false for nonexistent item")
+	}
+
+	// Test after removal
+	pq.Remove("req1")
+	if pq.Contains("req1") {
+		t.Error("Expected Contains to return false after removal")
+	}
+}
+
+func TestClone(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Test clone of empty queue
+	clone := pq.Clone()
+	if clone.GetSize() != 0 {
+		t.Error("Expected cloned empty queue to be empty")
+	}
+
+	// Add items to original
+	pq.Add("req1", 1.0)
+	pq.Add("req2", 2.0)
+	pq.Add("req3", 3.0)
+
+	// Clone with items
+	clone = pq.Clone()
+
+	// Verify clone has same items
+	if clone.GetSize() != pq.GetSize() {
+		t.Errorf("Expected clone size %d, got %d", pq.GetSize(), clone.GetSize())
+	}
+
+	// Verify independence - modify original
+	pq.Add("req4", 4.0)
+	if clone.GetSize() == pq.GetSize() {
+		t.Error("Clone should be independent of original")
+	}
+
+	// Verify independence - modify clone
+	clone.Remove("req1")
+	if !pq.Contains("req1") {
+		t.Error("Original should not be affected by clone modifications")
+	}
+
+	// Verify deep copy - items should be different instances
+	origPeek := pq.Peek()
+	clonePeek := clone.Peek()
+	if origPeek == clonePeek {
+		t.Error("Clone should create new Request instances, not share pointers")
+	}
+}
+
+func TestString(t *testing.T) {
+	pq := newRequestPriorityQueue()
+
+	// Test empty queue
+	str := pq.String()
+	expected := "RequestPriorityQueue: []"
+	if str != expected {
+		t.Errorf("Expected %q, got %q", expected, str)
+	}
+
+	// Test with items
+	pq.Add("req1", 1.5)
+	pq.Add("req2", 2.25)
+
+	str = pq.String()
+	// Should contain both items in priority order
+	if !contains(str, "req1(1.50)") || !contains(str, "req2(2.25)") {
+		t.Errorf("String output missing expected items: %s", str)
+	}
+}
+
+func TestConcurrency(t *testing.T) {
+	pq := newRequestPriorityQueue()
+	const numWorkers = 10
+	const itemsPerWorker = 100
+
+	var wg sync.WaitGroup
+
+	// Launch workers that add items
+	for i := 0; i < numWorkers; i++ {
+		wg.Add(1)
+		go func(workerID int) {
+			defer wg.Done()
+			for j := 0; j < itemsPerWorker; j++ {
+				id := fmt.Sprintf("worker%d-item%d", workerID, j)
+				tpot := float64(j) + float64(workerID)*0.1
+				pq.Add(id, tpot)
+			}
+		}(i)
+	}
+
+	// Launch workers that read from the queue
+	for i := 0; i < numWorkers; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			for j := 0; j < itemsPerWorker/2; j++ {
+				pq.Peek()
+				pq.GetSize()
+				time.Sleep(time.Microsecond)
+			}
+		}()
+	}
+
+	wg.Wait()
+
+	// Verify final state
+	expectedSize := numWorkers * itemsPerWorker
+	if pq.GetSize() != expectedSize {
+		t.Errorf("Expected final size %d, got %d", expectedSize, pq.GetSize())
+	}
+}
+
+func TestLargeQueue(t *testing.T) {
+	pq := newRequestPriorityQueue()
+	const numItems = 10000
+
+	// Add many items
+	for i := 0; i < numItems; i++ {
+		id := fmt.Sprintf("item%d", i)
+		tpot := float64(numItems - i) // Reverse order so item0 has highest priority
+		pq.Add(id, tpot)
+	}
+
+	if pq.GetSize() != numItems {
+		t.Errorf("Expected size %d, got %d", numItems, pq.GetSize())
+	}
+
+	// Verify priority ordering by removing items
+	lastTPOT := -1.0
+	for i := 0; i < numItems; i++ {
+		item := pq.Peek()
+		if item.tpot < lastTPOT {
+			t.Errorf("Priority order violated: %.1f < %.1f", item.tpot, lastTPOT)
+		}
+		lastTPOT = item.tpot
+		pq.Remove(item.id)
+	}
+
+	if pq.GetSize() != 0 {
+		t.Errorf("Expected empty queue after removing all items, got size %d", pq.GetSize())
+	}
+}
+
+func BenchmarkAdd(b *testing.B) {
+	pq := newRequestPriorityQueue()
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		id := fmt.Sprintf("item%d", i)
+		pq.Add(id, float64(i))
+	}
+}
+
+func BenchmarkPeek(b *testing.B) {
+	pq := newRequestPriorityQueue()
+
+	// Pre-populate queue
+	for i := 0; i < 1000; i++ {
+		pq.Add(fmt.Sprintf("item%d", i), float64(i))
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		pq.Peek()
+	}
+}
+
+func BenchmarkRemove(b *testing.B) {
+	pq := newRequestPriorityQueue()
+
+	// Pre-populate queue
+	for i := 0; i < b.N; i++ {
+		pq.Add(fmt.Sprintf("item%d", i), float64(i))
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		pq.Remove(fmt.Sprintf("item%d", i))
+	}
+}
+
+// Helper function to check if a string contains a substring
+func contains(s, substr string) bool {
+	return len(s) >= len(substr) &&
+		(s == substr ||
+			s[:len(substr)] == substr ||
+			s[len(s)-len(substr):] == substr ||
+			containsHelper(s, substr))
+}
+
+func containsHelper(s, substr string) bool {
+	for i := 0; i <= len(s)-len(substr); i++ {
+		if s[i:i+len(substr)] == substr {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer_test.go b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer_test.go
new file mode 100644
index 0000000..a15cb29
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/multi/slo_aware_router/scorer_test.go
@@ -0,0 +1,516 @@
+/*
+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 slo_aware_router
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"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"
+	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
+	latencypredictor "sigs.k8s.io/gateway-api-inference-extension/sidecars/latencypredictorasync"
+)
+
+// mockPredictor implements PredictorInterface for testing
+type mockPredictor struct {
+	predictions map[string]*latencypredictor.PredictionResponse
+	err         error
+}
+
+func (m *mockPredictor) Predict(ctx context.Context, request latencypredictor.PredictionRequest) (*latencypredictor.PredictionResponse, error) {
+	if m.err != nil {
+		return nil, m.err
+	}
+	// Generate a key based on KV cache percentage to return different predictions for different pods
+	key := fmt.Sprintf("%.1f", request.KVCachePercentage)
+	if pred, ok := m.predictions[key]; ok {
+		return pred, nil
+	}
+	// Default prediction
+	return &latencypredictor.PredictionResponse{TTFT: 0.5, TPOT: 0.03}, nil
+}
+
+func (m *mockPredictor) PredictBulk(ctx context.Context, requests []latencypredictor.PredictionRequest) (*latencypredictor.BulkPredictionResponse, error) {
+	if m.err != nil {
+		return nil, m.err
+	}
+	// Generate a key based on KV cache percentage to return different predictions for different pods
+	responses := make([]latencypredictor.PredictionResponse, 0, len(requests))
+	for _, request := range requests {
+		key := fmt.Sprintf("%.1f", request.KVCachePercentage)
+		if pred, ok := m.predictions[key]; ok {
+			responses = append(responses, *pred)
+		} else {
+			return nil, fmt.Errorf("no prediction for key %s", key)
+		}
+	}
+	return &latencypredictor.BulkPredictionResponse{Predictions: responses}, nil
+}
+
+func (m *mockPredictor) PredictBulkStrict(ctx context.Context, requests []latencypredictor.PredictionRequest) (*latencypredictor.BulkPredictionResponse, error) {
+	if m.err != nil {
+		return nil, m.err
+	}
+	// Generate a key based on KV cache percentage to return different predictions for different pods
+	responses := make([]latencypredictor.PredictionResponse, 0, len(requests))
+	for _, request := range requests {
+		key := fmt.Sprintf("%.1f", request.KVCachePercentage)
+		if pred, ok := m.predictions[key]; ok {
+			responses = append(responses, *pred)
+		} else {
+			return nil, fmt.Errorf("no prediction for key %s", key)
+		}
+	}
+	return &latencypredictor.BulkPredictionResponse{Predictions: responses}, nil
+}
+
+func (m *mockPredictor) AddTrainingDataBulk(data []latencypredictor.TrainingEntry) error {
+	return nil
+}
+
+func (m *mockPredictor) AddTrainingData(data latencypredictor.TrainingEntry) error {
+	return nil
+}
+
+func (m *mockPredictor) HealthCheck() error {
+	return nil
+}
+
+func (m *mockPredictor) GetServerStatus(ctx context.Context) (*latencypredictor.ServerStatusResponse, error) {
+	return &latencypredictor.ServerStatusResponse{}, nil
+}
+
+func createTestPod(name string, kvCacheUsage float64, runningQueueSize, waitingQueueSize int) schedulingtypes.Pod {
+	return &schedulingtypes.PodMetrics{
+		Pod: &backend.Pod{
+			NamespacedName: types.NamespacedName{
+				Name:      name,
+				Namespace: "default",
+			},
+		},
+		MetricsState: &backendmetrics.MetricsState{
+			KVCacheUsagePercent: kvCacheUsage,
+			RunningQueueSize:    runningQueueSize,
+			WaitingQueueSize:    waitingQueueSize,
+		},
+	}
+}
+
+func createTestLLMRequest(reqID string, ttftSLO, tpotSLO float64, predictionBased bool) *schedulingtypes.LLMRequest {
+	headers := make(map[string]string)
+	headers[requtil.RequestIdHeaderKey] = reqID
+	if ttftSLO > 0 {
+		headers["x-ttft-slo"] = fmt.Sprintf("%f", ttftSLO)
+	}
+	if tpotSLO > 0 {
+		headers["x-avg-tpot-slo"] = fmt.Sprintf("%f", tpotSLO)
+	}
+	headers["x-prediction-based-scheduling"] = strconv.FormatBool(predictionBased)
+
+	return &schedulingtypes.LLMRequest{
+		Headers: headers,
+		Body: &schedulingtypes.LLMRequestBody{
+			Completions: &schedulingtypes.CompletionsRequest{
+				Prompt: "test prompt",
+			},
+		},
+	}
+}
+
+func TestSLOAwareRouter_Score(t *testing.T) {
+	tests := []struct {
+		name           string
+		predictor      *mockPredictor
+		strategy       headroomStrategy
+		request        *schedulingtypes.LLMRequest
+		pods           []schedulingtypes.Pod
+		expectedScores map[string]float64 // Map of pod name to expected score
+		expectNil      bool
+	}{
+		{
+			name:      "Prediction-based scheduling disabled",
+			predictor: &mockPredictor{},
+			strategy:  headroomStrategyLeast,
+			request:   createTestLLMRequest("test", 1.0, 0.05, false), // predictionBased = false
+			pods: []schedulingtypes.Pod{
+				createTestPod("pod1", 0.5, 2, 1), // 50% KV cache, 2 running, 1 waiting
+				createTestPod("pod2", 0.7, 3, 2), // 70% KV cache, 3 running, 2 waiting
+			},
+			expectNil: true,
+		},
+		{
+			name:      "No predictor configured",
+			predictor: nil,
+			strategy:  headroomStrategyLeast,
+			request:   createTestLLMRequest("test", 1.0, 0.05, true),
+			pods: []schedulingtypes.Pod{
+				createTestPod("pod1", 0.5, 2, 1),
+			},
+			expectNil: true,
+		},
+		{
+			name: "All pods have positive headroom",
+			predictor: &mockPredictor{
+				predictions: map[string]*latencypredictor.PredictionResponse{
+					"0.5": {TTFT: 0.5, TPOT: 0.03}, // 50% KV cache
+					"0.6": {TTFT: 0.6, TPOT: 0.04}, // 60% KV cache
+					"0.3": {TTFT: 0.4, TPOT: 0.02}, // 30% KV cache
+				},
+			},
+			strategy: headroomStrategyLeast,
+			request:  createTestLLMRequest("test", 1.0, 0.05, true),
+			pods: []schedulingtypes.Pod{
+				createTestPod("pod1", 0.5, 2, 1), // 50% KV cache
+				createTestPod("pod2", 0.6, 3, 2), // 60% KV cache
+				createTestPod("pod3", 0.3, 1, 0), // 30% KV cache
+			},
+			// One pod should be selected with score 1, others 0
+			expectedScores: map[string]float64{
+				// We can't predict which one due to randomness, but exactly one should be 1
+			},
+		},
+		{
+			name: "All pods have negative headroom",
+			predictor: &mockPredictor{
+				predictions: map[string]*latencypredictor.PredictionResponse{
+					"0.8": {TTFT: 1.5, TPOT: 0.08}, // 80% KV cache - high load
+					"0.9": {TTFT: 1.8, TPOT: 0.09}, // 90% KV cache - very high load
+				},
+			},
+			strategy: headroomStrategyLeast,
+			request:  createTestLLMRequest("test", 1.0, 0.05, true),
+			pods: []schedulingtypes.Pod{
+				createTestPod("pod1", 0.8, 5, 3), // 80% KV cache, high load
+				createTestPod("pod2", 0.9, 6, 4), // 90% KV cache, very high load
+			},
+			// One pod should still be selected even with negative headroom
+			expectedScores: map[string]float64{},
+		},
+		{
+			name: "Mixed positive and negative headroom",
+			predictor: &mockPredictor{
+				predictions: map[string]*latencypredictor.PredictionResponse{
+					"0.3": {TTFT: 0.5, TPOT: 0.03}, // 30% KV cache - Positive headroom
+					"0.9": {TTFT: 1.5, TPOT: 0.08}, // 90% KV cache - Negative headroom
+				},
+			},
+			strategy: headroomStrategyLeast,
+			request:  createTestLLMRequest("test", 1.0, 0.05, true),
+			pods: []schedulingtypes.Pod{
+				createTestPod("pod-positive", 0.3, 1, 0), // Low KV cache, positive headroom
+				createTestPod("pod-negative", 0.9, 6, 4), // High KV cache, negative headroom
+			},
+			// With 99% probability, positive headroom pod should be selected
+			expectedScores: map[string]float64{},
+		},
+		{
+			name: "Prediction errors - fallback to composite scoring",
+			predictor: &mockPredictor{
+				err: errors.New("prediction failed"),
+			},
+			strategy: headroomStrategyLeast,
+			request:  createTestLLMRequest("test", 1.0, 0.05, true),
+			pods: []schedulingtypes.Pod{
+				createTestPod("pod1", 0.5, 2, 1),
+				createTestPod("pod2", 0.6, 3, 2),
+			},
+			// Should fall back to composite-only scoring and select one pod
+			expectedScores: map[string]float64{
+				// One pod should be selected with score 1, verified in general validation below
+			},
+		},
+		{
+			name:      "Empty pod list",
+			predictor: &mockPredictor{},
+			strategy:  headroomStrategyLeast,
+			request:   createTestLLMRequest("test", 1.0, 0.05, true),
+			pods:      []schedulingtypes.Pod{},
+			// Should return empty scores map
+			expectedScores: map[string]float64{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var router *SLOAwareRouter
+			if tt.predictor == nil {
+				router = NewSLOAwareRouter(nil, tt.strategy)
+			} else {
+				router = NewSLOAwareRouter(tt.predictor, tt.strategy)
+			}
+
+			scores := router.Score(context.Background(), schedulingtypes.NewCycleState(), tt.request, tt.pods)
+
+			if tt.expectNil {
+				assert.Nil(t, scores, "Expected nil scores")
+				return
+			}
+
+			assert.NotNil(t, scores, "Expected non-nil scores")
+
+			// If we have specific expected scores, verify them
+			if len(tt.expectedScores) > 0 {
+				for _, pod := range tt.pods {
+					podName := pod.GetPod().NamespacedName.Name
+					if expectedScore, ok := tt.expectedScores[podName]; ok {
+						assert.InDelta(t, expectedScore, scores[pod], 0.0001, "Pod %s should have score %f", podName, expectedScore)
+					}
+				}
+			}
+
+			// General validation: exactly one pod should have score 1 (selected), others should have score 0
+			// This applies even when predictions fail because we fall back to composite scoring
+			if !tt.expectNil && len(tt.pods) > 0 && tt.predictor != nil {
+				selectedCount := 0
+				for _, score := range scores {
+					if score == 1.0 {
+						selectedCount++
+					} else {
+						assert.InDelta(t, 0.0, score, 0.0001, "Non-selected pods should have score 0")
+					}
+				}
+				assert.Equal(t, 1, selectedCount, "Exactly one pod should be selected with score 1")
+			}
+		})
+	}
+}
+
+func TestSLOAwareRouter_Strategies(t *testing.T) {
+	tests := []struct {
+		name     string
+		strategy headroomStrategy
+	}{
+		{
+			name:     "HeadroomStrategyLeast",
+			strategy: headroomStrategyLeast,
+		},
+		{
+			name:     "HeadroomStrategyMost",
+			strategy: headroomStrategyMost,
+		},
+		{
+			name:     "HeadroomStrategyCompositeMost",
+			strategy: headroomStrategyCompositeMost,
+		},
+		{
+			name:     "HeadroomStrategyCompositeLeast",
+			strategy: headroomStrategyCompositeLeast,
+		},
+		{
+			name:     "HeadroomStrategyCompositeOnly",
+			strategy: headroomStrategyCompositeOnly,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			predictor := &mockPredictor{
+				predictions: map[string]*latencypredictor.PredictionResponse{
+					"0.5": {TTFT: 0.5, TPOT: 0.03},
+					"0.6": {TTFT: 0.6, TPOT: 0.04},
+					"0.3": {TTFT: 0.4, TPOT: 0.02},
+				},
+			}
+			router := NewSLOAwareRouter(predictor, tt.strategy)
+
+			request := createTestLLMRequest("test", 1.0, 0.05, true)
+			pods := []schedulingtypes.Pod{
+				createTestPod("pod1", 0.5, 2, 1),
+				createTestPod("pod2", 0.6, 3, 2),
+				createTestPod("pod3", 0.3, 1, 0),
+			}
+
+			scores := router.Score(context.Background(), schedulingtypes.NewCycleState(), request, pods)
+
+			assert.NotNil(t, scores, "Expected non-nil scores for strategy %s", tt.strategy)
+
+			// Verify exactly one pod is selected
+			selectedCount := 0
+			for _, score := range scores {
+				if score == 1.0 {
+					selectedCount++
+				}
+			}
+			assert.Equal(t, 1, selectedCount, "Strategy %s should select exactly one pod", tt.strategy)
+		})
+	}
+}
+
+func TestSLOAwareRouter_TypedName(t *testing.T) {
+	predictor := &mockPredictor{}
+	router := NewSLOAwareRouter(predictor, headroomStrategyLeast)
+
+	tn := router.TypedName()
+	assert.Equal(t, "slo-aware-routing", tn.Type, "Type should be slo-aware-routing")
+	assert.Equal(t, "slo-aware-routing", tn.Name, "Default name should be slo-aware-routing")
+}
+
+func TestSLOAwareRouter_WithName(t *testing.T) {
+	predictor := &mockPredictor{}
+	router := NewSLOAwareRouter(predictor, headroomStrategyLeast)
+
+	customName := "custom-router"
+	router = router.WithName(customName)
+
+	tn := router.TypedName()
+	assert.Equal(t, "slo-aware-routing", tn.Type, "Type should remain slo-aware-routing")
+	assert.Equal(t, customName, tn.Name, "Name should be updated to custom name")
+}
+
+func TestSLOAwareRouter_GetPodRunningRequestCount(t *testing.T) {
+	tests := []struct {
+		name          string
+		setupRequests func(*SLOAwareRouter, schedulingtypes.Pod)
+		expectedCount int
+	}{
+		{
+			name:          "No running requests",
+			setupRequests: func(r *SLOAwareRouter, p schedulingtypes.Pod) {},
+			expectedCount: 0,
+		},
+		{
+			name: "One running request",
+			setupRequests: func(r *SLOAwareRouter, p schedulingtypes.Pod) {
+				podName := types.NamespacedName{
+					Name:      p.GetPod().NamespacedName.Name,
+					Namespace: p.GetPod().NamespacedName.Namespace,
+				}
+				r.runningRequestLists[podName] = newRequestPriorityQueue()
+				r.runningRequestLists[podName].Add("req1", 0.04)
+			},
+			expectedCount: 1,
+		},
+		{
+			name: "Multiple running requests",
+			setupRequests: func(r *SLOAwareRouter, p schedulingtypes.Pod) {
+				podName := types.NamespacedName{
+					Name:      p.GetPod().NamespacedName.Name,
+					Namespace: p.GetPod().NamespacedName.Namespace,
+				}
+				r.runningRequestLists[podName] = newRequestPriorityQueue()
+				r.runningRequestLists[podName].Add("req1", 0.04)
+				r.runningRequestLists[podName].Add("req2", 0.03)
+				r.runningRequestLists[podName].Add("req3", 0.05)
+			},
+			expectedCount: 3,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			predictor := &mockPredictor{}
+			router := NewSLOAwareRouter(predictor, headroomStrategyLeast)
+			pod := createTestPod("test-pod", 0.5, 2, 1)
+
+			tt.setupRequests(router, pod)
+
+			count := router.getPodRunningRequestCount(pod)
+			assert.Equal(t, tt.expectedCount, count, "Running request count should match expected")
+		})
+	}
+}
+
+func TestSLOAwareRouter_GetPodMinTPOTSLO(t *testing.T) {
+	tests := []struct {
+		name          string
+		setupRequests func(*SLOAwareRouter, schedulingtypes.Pod)
+		expectedSLO   float64
+	}{
+		{
+			name:          "No running requests",
+			setupRequests: func(r *SLOAwareRouter, p schedulingtypes.Pod) {},
+			expectedSLO:   0.0,
+		},
+		{
+			name: "One running request",
+			setupRequests: func(r *SLOAwareRouter, p schedulingtypes.Pod) {
+				podName := types.NamespacedName{
+					Name:      p.GetPod().NamespacedName.Name,
+					Namespace: p.GetPod().NamespacedName.Namespace,
+				}
+				r.runningRequestLists[podName] = newRequestPriorityQueue()
+				r.runningRequestLists[podName].Add("req1", 0.04)
+			},
+			expectedSLO: 0.04,
+		},
+		{
+			name: "Multiple running requests - should return minimum",
+			setupRequests: func(r *SLOAwareRouter, p schedulingtypes.Pod) {
+				podName := types.NamespacedName{
+					Name:      p.GetPod().NamespacedName.Name,
+					Namespace: p.GetPod().NamespacedName.Namespace,
+				}
+				r.runningRequestLists[podName] = newRequestPriorityQueue()
+				// Add in any order - heap will maintain minimum at top
+				r.runningRequestLists[podName].Add("req1", 0.05)
+				r.runningRequestLists[podName].Add("req2", 0.03) // This is the minimum
+				r.runningRequestLists[podName].Add("req3", 0.04)
+			},
+			expectedSLO: 0.03, // Minimum TPOT (heap guarantees this is at items[0])
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			predictor := &mockPredictor{}
+			router := NewSLOAwareRouter(predictor, headroomStrategyLeast)
+			pod := createTestPod("test-pod", 0.5, 2, 1)
+
+			tt.setupRequests(router, pod)
+
+			minSLO := router.getPodMinTPOTSLO(pod)
+			assert.InDelta(t, tt.expectedSLO, minSLO, 0.0001, "Min TPOT SLO should match expected")
+		})
+	}
+}
+
+func TestSLOAwareRouter_GetPrefixCacheScoreForPod(t *testing.T) {
+	tests := []struct {
+		name          string
+		setupState    func(*schedulingtypes.CycleState)
+		expectedScore float64
+	}{
+		{
+			name:          "No prefix cache state",
+			setupState:    func(s *schedulingtypes.CycleState) {},
+			expectedScore: 0.0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			predictor := &mockPredictor{}
+			router := NewSLOAwareRouter(predictor, headroomStrategyLeast)
+
+			state := schedulingtypes.NewCycleState()
+			tt.setupState(state)
+
+			pod := createTestPod("test-pod", 0.5, 2, 1)
+
+			score := router.getPrefixCacheScoreForPod(context.Background(), state, pod)
+			assert.InDelta(t, tt.expectedScore, score, 0.0001, "Prefix cache score should match expected")
+		})
+	}
+}
diff --git a/pkg/epp/server/runserver_test.go b/pkg/epp/server/runserver_test.go
index b02688c..1729288 100644
--- a/pkg/epp/server/runserver_test.go
+++ b/pkg/epp/server/runserver_test.go
@@ -20,7 +20,6 @@ import (
 	"testing"
 
 	"sigs.k8s.io/controller-runtime/pkg/manager"
-
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
diff --git a/pkg/epp/util/testing/wrappers.go b/pkg/epp/util/testing/wrappers.go
index 7621bff..3e33805 100644
--- a/pkg/epp/util/testing/wrappers.go
+++ b/pkg/epp/util/testing/wrappers.go
@@ -19,8 +19,12 @@ package testing
 import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
 )
 
 // PodWrapper wraps a Pod.
@@ -219,6 +223,19 @@ func (m *InferencePoolWrapper) ObjRef() *v1.InferencePool {
 	return &m.InferencePool
 }
 
+func (m *InferencePoolWrapper) ToGKNN() common.GKNN {
+	return common.GKNN{
+		NamespacedName: types.NamespacedName{
+			Name:      m.Name,
+			Namespace: m.ObjectMeta.Namespace,
+		},
+		GroupKind: schema.GroupKind{
+			Group: "inference.networking.k8s.io",
+			Kind:  "InferencePool",
+		},
+	}
+}
+
 // AlphaInferencePoolWrapper wraps an group "inference.networking.x-k8s.io" InferencePool.
 type AlphaInferencePoolWrapper struct {
 	v1alpha2.InferencePool
diff --git a/test/integration/epp/hermetic_test.go b/test/integration/epp/hermetic_test.go
index 9ce4fec..f1f32d5 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -48,6 +48,7 @@ import (
 	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	metricsutils "k8s.io/component-base/metrics/testutil"
+
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -56,8 +57,6 @@ import (
 	crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
 	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
-	"sigs.k8s.io/yaml"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/common"
@@ -79,6 +78,7 @@ import (
 	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
 	epptestutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 	integrationutils "sigs.k8s.io/gateway-api-inference-extension/test/integration"
+	"sigs.k8s.io/yaml"
 )
 
 const (
@@ -1170,10 +1170,11 @@ func BeforeSuite() func() {
 	serverRunner.TestPodMetricsClient = &backendmetrics.FakePodMetricsClient{}
 	pmf := backendmetrics.NewPodMetricsFactory(serverRunner.TestPodMetricsClient, 10*time.Millisecond)
 	// Adjust from defaults
-	serverRunner.PoolGKNN = common.GKNN{
+	serverRunner.GKNN = common.GKNN{
 		NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPoolName},
 		GroupKind:      schema.GroupKind{Group: v1.GroupVersion.Group, Kind: "InferencePool"},
 	}
+
 	serverRunner.Datastore = datastore.NewDatastore(context.Background(), pmf, 0)
 
 	kvCacheUtilizationScorer := scorer.NewKVCacheUtilizationScorer()
diff --git a/test/utils/server.go b/test/utils/server.go
index 9cf907d..f46ed6f 100644
--- a/test/utils/server.go
+++ b/test/utils/server.go
@@ -30,13 +30,14 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
+	pooltuil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pool"
 	testutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 )
 
@@ -72,7 +73,7 @@ func PrepareForTestStreamingServer(objectives []*v1alpha2.InferenceObjective, po
 		Build()
 	pool := testutil.MakeInferencePool(poolName).Namespace(namespace).ObjRef()
 	pool.Spec.TargetPorts = []v1.Port{{Number: v1.PortNumber(poolPort)}}
-	_ = ds.PoolSet(context.Background(), fakeClient, pool)
+	_ = ds.PoolSet(context.Background(), fakeClient, pooltuil.InferencePoolToEndpointPool(pool))
 
 	return ctx, cancel, ds, pmc
 }