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

Issue:
Configure x-request-id support in the default ootb examples
We should configure x-request-id correctly for our out of the box examples, and ensure that gateways can easily then integrate their own request tracing.

For vLLM, it would be very desirable to ensure that x-request-id is sufficiently scoped in logs such that we can identify request start, request end, and any related requests (i.e. `POST /v1/completions` or similar should show the request id)

Hints from issue comments:
@kfswain: This should be reasonable for a newcomer to handle, we just need to expose x-request-id in logs@delavet: Hi maintainers (●ﾟωﾟ●), I'm Hang Yin from Alibaba Cloud and I would like to start contributing from this issue. Please assign it to me.
/assign

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index 8fb00dfb..9cb62e28 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@ FROM ${BUILDER_IMAGE} AS builder
 ENV CGO_ENABLED=0
 ENV GOOS=linux
 ENV GOARCH=amd64
+ARG COMMIT_SHA=unknown
 
 # Dependencies
 WORKDIR /src
@@ -20,7 +21,7 @@ COPY pkg ./pkg
 COPY internal ./internal
 COPY api ./api
 WORKDIR /src/cmd/epp
-RUN go build -o /epp
+RUN go build -ldflags="-X sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics.CommitSHA=${COMMIT_SHA}" -o /epp
 
 ## Multistage deploy
 FROM ${BASE_IMAGE}
diff --git a/Makefile b/Makefile
index 563e0ce9..884d4229 100644
--- a/Makefile
+++ b/Makefile
@@ -21,6 +21,7 @@ CONTAINER_TOOL ?= docker
 SHELL = /usr/bin/env bash -o pipefail
 .SHELLFLAGS = -ec
 
+GIT_COMMIT_SHA ?= "$(shell git rev-parse HEAD 2>/dev/null)"
 GIT_TAG ?= $(shell git describe --tags --dirty --always)
 PLATFORMS ?= linux/amd64
 DOCKER_BUILDX_CMD ?= docker buildx
@@ -121,7 +122,7 @@ vet: ## Run go vet against code.
 
 .PHONY: test
 test: manifests generate fmt vet envtest image-build ## Run tests.
-	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -race -coverprofile cover.out
+	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e | grep -v /conformance) -race -coverprofile cover.out
 
 .PHONY: test-unit
 test-unit: ## Run unit tests.
@@ -175,6 +176,7 @@ image-build: ## Build the EPP image using Docker Buildx.
 		--platform=$(PLATFORMS) \
 		--build-arg BASE_IMAGE=$(BASE_IMAGE) \
 		--build-arg BUILDER_IMAGE=$(BUILDER_IMAGE) \
+		--build-arg COMMIT_SHA=${GIT_COMMIT_SHA} \
 		$(PUSH) \
 		$(LOAD) \
 		$(IMAGE_BUILD_EXTRA_OPTS) ./
diff --git a/cloudbuild.yaml b/cloudbuild.yaml
index 6043d225..f05c8c00 100644
--- a/cloudbuild.yaml
+++ b/cloudbuild.yaml
@@ -12,6 +12,7 @@ steps:
     - GIT_TAG=$_GIT_TAG
     - EXTRA_TAG=$_PULL_BASE_REF
     - DOCKER_BUILDX_CMD=/buildx-entrypoint
+    - GIT_COMMIT_SHA=$COMMIT_SHA
   - name: gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20240718-5ef92b5c36
     entrypoint: make
     args:
diff --git a/cmd/epp/main.go b/cmd/epp/main.go
index c0a87e62..2bd779c5 100644
--- a/cmd/epp/main.go
+++ b/cmd/epp/main.go
@@ -41,6 +41,7 @@ import (
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
 	runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
@@ -169,6 +170,7 @@ func run() error {
 
 	datastore := datastore.NewDatastore(ctx, pmf)
 
+	scheduler := scheduling.NewScheduler(datastore)
 	serverRunner := &runserver.ExtProcServerRunner{
 		GrpcPort:                                 *grpcPort,
 		DestinationEndpointHintMetadataNamespace: *destinationEndpointHintMetadataNamespace,
@@ -178,6 +180,7 @@ func run() error {
 		SecureServing:                            *secureServing,
 		CertPath:                                 *certPath,
 		RefreshPrometheusMetricsInterval:         *refreshPrometheusMetricsInterval,
+		Scheduler:                                scheduler,
 	}
 	if err := serverRunner.SetupWithManager(ctx, mgr); err != nil {
 		setupLog.Error(err, "Failed to setup ext-proc controllers")
@@ -247,6 +250,8 @@ func registerHealthServer(mgr manager.Manager, logger logr.Logger, ds datastore.
 func registerMetricsHandler(mgr manager.Manager, port int, cfg *rest.Config) error {
 	metrics.Register()
 
+	metrics.RecordInferenceExtensionInfo()
+
 	// Init HTTP server.
 	h, err := metricsHandlerWithAuthenticationAndAuthorization(cfg)
 	if err != nil {
diff --git a/conformance/conformance.go b/conformance/conformance.go
new file mode 100644
index 00000000..20d80fde
--- /dev/null
+++ b/conformance/conformance.go
@@ -0,0 +1,230 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package conformance contains the core setup and execution logic
+// for the Gateway API Inference Extension conformance test suite.
+package conformance
+
+import (
+	"fmt"
+	"io/fs"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	clientset "k8s.io/client-go/kubernetes"
+
+	// Import runtime package for scheme creation
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/yaml"
+
+	// Import necessary types and utilities from the core Gateway API conformance suite.
+	// Assumes sigs.k8s.io/gateway-api is a dependency in the go.mod.
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"            // Import core Gateway API types
+	confapis "sigs.k8s.io/gateway-api/conformance/apis/v1" // Report struct definition
+	confconfig "sigs.k8s.io/gateway-api/conformance/utils/config"
+	confflags "sigs.k8s.io/gateway-api/conformance/utils/flags"
+	confsuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
+	"sigs.k8s.io/gateway-api/pkg/features" // Using core features definitions if applicable
+
+	// Import the test definitions package to access the ConformanceTests slice
+	"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
+
+	// Import test packages using blank identifier
+	// This triggers the init() functions in these packages, which register the tests
+	// by appending them to the tests.ConformanceTests slice.
+	_ "sigs.k8s.io/gateway-api-inference-extension/conformance/tests/basic"
+	// TODO: Add blank imports for other test categories as they are created.
+	// _ "sigs.k8s.io/gateway-api-inference-extension/conformance/tests/model_routing"
+
+	// Import the Inference Extension API types
+	inferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
+)
+
+// GatewayLayerProfileName defines the name for the conformance profile that tests
+// the Gateway API layer aspects of the Inference Extension (e.g., InferencePool, InferenceModel CRDs).
+// Future profiles will cover EPP and ModelServer layers.
+const GatewayLayerProfileName confsuite.ConformanceProfileName = "Gateway"
+
+var InferenceCoreFeatures = sets.New[features.FeatureName]() // Placeholder - Populate with actual features specific to this profile or manage features per profile
+
+// GatewayLayerProfile defines the conformance profile for the Gateway API layer
+// of the Inference Extension.
+// In future iterations, we will add constants and ConformanceProfile structs for
+// EPPProfileName ("EPP") and ModelServerProfileName ("ModelServer")
+// to cover their respective conformance layers.
+var GatewayLayerProfile = confsuite.ConformanceProfile{
+	Name:         GatewayLayerProfileName,
+	CoreFeatures: InferenceCoreFeatures,
+}
+
+// DefaultOptions parses command line flags and sets up the suite options.
+// Adapted from the core Gateway API conformance suite.
+func DefaultOptions(t *testing.T) confsuite.ConformanceOptions {
+	t.Helper()
+
+	cfg, err := config.GetConfig()
+	require.NoError(t, err, "error loading Kubernetes config")
+
+	// Initialize client options. The scheme must include Gateway API types
+	// and the Inference Extension types.
+	clientOptions := client.Options{}
+	scheme := clientOptions.Scheme
+	if scheme == nil {
+		// If default options don't provide a scheme, create one using runtime.NewScheme().
+		scheme = runtime.NewScheme()
+		clientOptions.Scheme = scheme
+	}
+
+	// Register necessary API Types
+	require.NoError(t, gatewayv1.Install(scheme)) // Add core Gateway API types
+	// Add the Inference Extension API types to the scheme using the correct import alias
+	require.NoError(t, inferencev1alpha2.Install(scheme))
+	require.NoError(t, apiextensionsv1.AddToScheme(scheme)) // Needed for CRD checks
+
+	// Create the Kubernetes clients
+	c, err := client.New(cfg, clientOptions)
+	require.NoError(t, err, "error initializing Kubernetes client")
+	cs, err := clientset.NewForConfig(cfg)
+	require.NoError(t, err, "error initializing Kubernetes clientset")
+
+	exemptFeatures := confsuite.ParseSupportedFeatures(*confflags.ExemptFeatures)
+	skipTests := confsuite.ParseSkipTests(*confflags.SkipTests)
+	// Initially, run the GatewayLayerProfile. This will expand as other profiles
+	// (EPP, ModelServer) are added and can be selected via flags in future iterations.
+	conformanceProfiles := sets.New(GatewayLayerProfileName)
+
+	// Implementation details from flags
+	implementation := confsuite.ParseImplementation(
+		*confflags.ImplementationOrganization,
+		*confflags.ImplementationProject,
+		*confflags.ImplementationURL,
+		*confflags.ImplementationVersion,
+		*confflags.ImplementationContact,
+	)
+
+	// Inference Extension Specific Report Fields
+	inferenceExtensionVersion := "v0.3.0"
+	_ = inferenceExtensionVersion // Avoid unused variable error until implemented
+
+	// Create ConformanceOptions
+	opts := confsuite.ConformanceOptions{
+		Client:               c,
+		Clientset:            cs,
+		RestConfig:           cfg,
+		GatewayClassName:     *confflags.GatewayClassName,
+		Debug:                *confflags.ShowDebug,
+		CleanupBaseResources: *confflags.CleanupBaseResources,
+		SupportedFeatures:    sets.New[features.FeatureName](), // Initialize empty, will be populated below
+		TimeoutConfig:        confconfig.DefaultTimeoutConfig(),
+		SkipTests:            skipTests,
+		ExemptFeatures:       exemptFeatures,
+		RunTest:              *confflags.RunTest,
+		Mode:                 *confflags.Mode,
+		Implementation:       implementation,
+		ConformanceProfiles:  conformanceProfiles,
+		ManifestFS:           []fs.FS{&Manifests}, // Assumes embed.go defines `Manifests`
+		ReportOutputPath:     *confflags.ReportOutput,
+		SkipProvisionalTests: *confflags.SkipProvisionalTests,
+		// TODO: Add the inference extension specific fields to ConformanceOptions struct if needed,
+		// or handle them during report generation.
+		// GatewayAPIInferenceExtensionChannel: inferenceExtensionChannel,
+		// GatewayAPIInferenceExtensionVersion: inferenceExtensionVersion,
+	}
+
+	// Populate SupportedFeatures based on the GatewayLayerProfile.
+	// Since all features are mandatory for this profile, add all defined core features.
+	if opts.ConformanceProfiles.Has(GatewayLayerProfileName) {
+		for feature := range GatewayLayerProfile.CoreFeatures {
+			opts.SupportedFeatures.Insert(feature)
+		}
+	}
+
+	// Remove any features explicitly exempted via flags.
+	for feature := range opts.ExemptFeatures {
+		opts.SupportedFeatures.Delete(feature)
+	}
+
+	return opts
+}
+
+// RunConformance runs the Inference Extension conformance tests using default options.
+func RunConformance(t *testing.T) {
+	RunConformanceWithOptions(t, DefaultOptions(t))
+}
+
+// RunConformanceWithOptions runs the Inference Extension conformance tests with specific options.
+func RunConformanceWithOptions(t *testing.T, opts confsuite.ConformanceOptions) {
+	t.Logf("Running Inference Extension conformance tests with GatewayClass %s", opts.GatewayClassName)
+
+	// Register the GatewayLayerProfile with the suite runner.
+	// In the future, other profiles (EPP, ModelServer) will also be registered here,
+	// and the suite runner will execute tests based on the selected profiles.
+	confsuite.RegisterConformanceProfile(GatewayLayerProfile)
+
+	// Initialize the test suite.
+	cSuite, err := confsuite.NewConformanceTestSuite(opts)
+	require.NoError(t, err, "error initializing conformance suite")
+
+	t.Log("Setting up Inference Extension conformance tests")
+	// Setup requires the list of tests, which is populated by the init() functions
+	// triggered by the blank imports at the top of this file.
+	cSuite.Setup(t, tests.ConformanceTests)
+
+	t.Log("Running Inference Extension conformance tests")
+	// Run the tests.
+	err = cSuite.Run(t, tests.ConformanceTests)
+	require.NoError(t, err, "error running conformance tests")
+
+	// Generate and write the report if requested.
+	if opts.ReportOutputPath != "" {
+		t.Log("Generating Inference Extension conformance report")
+		report, err := cSuite.Report() // Use the existing report generation logic.
+		require.NoError(t, err, "error generating conformance report")
+
+		// TODO: Modify the report struct here if channel, version need to be modified.
+		// Example (requires adding fields to confapis.ConformanceReport):
+		// report.GatewayAPIInferenceExtensionChannel = opts.GatewayAPIInferenceExtensionChannel
+		// report.GatewayAPIInferenceExtensionVersion = opts.GatewayAPIInferenceExtensionVersion
+
+		err = writeReport(t.Logf, *report, opts.ReportOutputPath)
+		require.NoError(t, err, "error writing conformance report")
+	}
+}
+
+// writeReport writes the generated conformance report to the specified output file or logs it.
+// Adapted from the core Gateway API suite.
+func writeReport(logf func(string, ...any), report confapis.ConformanceReport, output string) error {
+	rawReport, err := yaml.Marshal(report)
+	if err != nil {
+		return fmt.Errorf("error marshaling report: %w", err)
+	}
+
+	if output != "" {
+		if err = os.WriteFile(output, rawReport, 0o600); err != nil {
+			return fmt.Errorf("error writing report file %s: %w", output, err)
+		}
+		logf("Conformance report written to %s", output)
+	} else {
+		// Log the report YAML to stdout if no output file is specified.
+		logf("Conformance report:\n%s", string(rawReport))
+	}
+	return nil
+}
diff --git a/conformance/embed.go b/conformance/embed.go
new file mode 100644
index 00000000..f7fa64c9
--- /dev/null
+++ b/conformance/embed.go
@@ -0,0 +1,25 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package conformance
+
+import "embed"
+
+// Manifests embeds the contents of the conformance/resources directory making
+// the YAML files within them available to the test suite at runtime.
+//
+//go:embed resources/* tests/*
+var Manifests embed.FS
diff --git a/conformance/reports/README.md b/conformance/reports/README.md
new file mode 100644
index 00000000..81652b1c
--- /dev/null
+++ b/conformance/reports/README.md
@@ -0,0 +1,93 @@
+# Conformance Reports for Gateway API Inference Extension
+
+This directory stores conformance reports submitted by various implementations of the Gateway API Inference Extension. This structure closely follows the [kubernetes-sigs/gateway-api/conformance/reports](https://github.com/kubernetes-sigs/gateway-api/blob/main/conformance/reports/README.md).
+
+## How this folder is structured
+
+This folder stores conformance reports organized first by the version of the Gateway API Inference Extension specification they were tested against, and then by the specific conformance profile (e.g., Gateway, EPP, Model Server):
+
+|-- conformance/reports
+|   |-- v0.3.0  # Example extension version
+|   |   |-- gateway  # Conformance profile/category
+|   |   |   |-- my-inference-gateway
+|   |   |   |   |-- README.md
+|   |   |   |   |-- experimental-v1.2.3-default-gateway-report.yaml # Example report file
+|   |   |   |-- another-implementation
+|   |   |   |   |-- README.md
+|   |   |   |   |-- ...
+|   |   |-- epp # Future conformance profile/category
+|   |   |   |-- my-epp-implementation
+|   |   |   |   |-- ...
+|   |   |-- model-server # Future conformance profile/category
+|   |   |   |-- ...
+|   |-- v0.4.0 # Future extension version
+|   |   |-- ...
+
+## Implementation Submissions
+
+Each implementation conformant with a specific profile of a specific version of the Gateway API Inference Extension should have its own folder within the corresponding version and profile directory (e.g., `/conformance/reports/v0.3.0/Gateway/my-implementation/`).
+
+The implementation is the owner of its folder and is responsible for:
+
+1.  Uploading one or more conformance reports (YAML files).
+2.  Maintaining a mandatory `README.md` file within their folder, structured as follows:
+
+    # My Inference Gateway Implementation (Gateway Profile Conformance)
+
+    General information about the My/Implementation project.
+
+    ## Table of Contents
+
+| Extension Version Tested | Profile Tested | Implementation Version | Mode    | Report                                                                     |
+|--------------------------|----------------|------------------------|---------|----------------------------------------------------------------------------|
+| v0.3.0                   | Gateway        | v1.2.3                 | default | [v1.2.3 Gateway report](./experimental-v1.2.3-default-gateway-report.yaml) |
+| ...                      | ...            | ...                    | ...     | ...                                                                        |
+
+    ## Reproduce
+
+    Instructions on how to reproduce the claimed report(s).
+
+### Table of Contents (within Implementation README)
+
+The table of contents within an implementation's `README.md` should contain one row for each submitted report and include the following columns:
+
+* **Extension Version Tested**: The version of the Gateway API Inference Extension specification tested against (e.g., `v0.3.0`). Must correspond to the `gatewayAPIInferenceExtensionVersion` field in the report.
+* **Profile Tested**: The specific conformance profile tested (e.g., `Gateway`, `EPP`, `ModelServer`). Must correspond to the `name` of the profile in the `profiles` list within the report.
+* **Implementation Version**: A link to the GitHub/website page for the specific release/commit of the implementation tested. The version value MUST correspond to the `implementation.version` field in the report.
+* **Mode**: The operating mode of the implementation used for the test run (default is `default`). Must correspond to the `mode` field in the report. If a mode other than `default` is used, the "Reproduce" section must explain how to configure it.
+* **Report**: A link to the corresponding report YAML file. Reports MUST be named according to the pattern: `<Implementation Version>-<Mode>-<Profile>-report.yaml` (e.g., `experimental-v1.2.3-default-gateway-report.yaml`).
+
+### Reproduce Section (within Implementation README)
+
+This section MUST exist and contain the manual or automatic steps required to reproduce the results claimed by the uploaded conformance reports for that specific implementation. If reproduction steps differ significantly between implementation versions, use sub-sections.
+
+## Report Files
+
+Conformance reports MUST be uploaded exactly as generated by the official Gateway API Inference Extension conformance test suite, without any modifications. The "Reproduce" section allows for verification of the submitted report against a fresh run.
+
+### Report Rules
+
+To be accepted, submitted conformance reports must comply with the following rules:
+
+1.  **Implementation Details:** All fields within the `implementation` block must have meaningful values:
+    * `organization`: The entity maintaining the implementation (company, open source org, individual).
+    * `project`: The name of the implementation project, unique within the organization.
+    * `url`: A valid URL for the project (e.g., GitHub repository, product page).
+    * `version`: A specific, reproducible snapshot of the implementation (e.g., tag, commit hash, release version). Branch names are not acceptable.
+    * `contact`: A list of contact points (GitHub handles like `@maintainer`, team handles like `@org/team`, email addresses, or support URLs like an issue tracker).
+2.  **Inference Extension Versioning:** The report MUST include:
+    * `gatewayAPIInferenceExtensionVersion`: The specific version of the Gateway API Inference Extension specification tested against (e.g., `v0.3.0`).
+3.  **Mode:** The `mode` field indicates the implementation's operating mode during the test run.
+4.  **Test Profile & Result:**
+    * The report MUST contain exactly one profile result under the `profiles` list for the specific conformance category being submitted (e.g., a report for "Gateway" conformance should only contain the "Gateway" profile result).
+    * The profile's `name` MUST match the conformance category (e.g., `Gateway`, `EPP`, `ModelServer`).
+    * The profile's `result` field MUST be `success`. A `success` result indicates that **all** tests defined within the Gateway API Inference Extension conformance suite for that specific profile and version passed.
+
+## Submission Process
+
+Conformance reports demonstrating a `success` result for a specific profile (e.g., `Gateway`) should be submitted via Pull Request directly to this repository (`kubernetes-sigs/gateway-api-inference-extension`).
+
+1.  Create a new folder structure under `/conformance/reports/<extension-version>/<profile-name>/` named after your implementation (e.g., `/conformance/reports/v0.3.0/Gateway/my-implementation/`).
+2.  Add your implementation's `README.md` to this folder, following the structure described above.
+3.  Add your generated conformance report YAML file(s) to this folder, ensuring they follow the naming convention `<Implementation Version>-<Mode>-<Profile>-report.yaml`.
+4.  Submit the Pull Request.
diff --git a/conformance/resources/manifests/manifests.yaml b/conformance/resources/manifests/manifests.yaml
new file mode 100644
index 00000000..7b43b784
--- /dev/null
+++ b/conformance/resources/manifests/manifests.yaml
@@ -0,0 +1,49 @@
+# Base Kubernetes resources for the Gateway API Inference Extension conformance tests.
+# This includes namespaces and a minimal set of resources (Gateway, Backend)
+# required by many tests. More specific resources should be defined within
+# individual test files or other resource directories (e.g., sample_backends).
+
+---
+# Namespace for core infrastructure like Gateways.
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: gateway-conformance-infra
+  labels:
+    gateway-conformance: infra
+
+---
+# Namespace for application backends (potentially simulating model servers
+# or where InferencePools might reside in some tests).
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: gateway-conformance-app-backend
+  labels:
+    gateway-conformance: backend
+
+---
+# A basic Gateway resource that allows HTTPRoutes from the same namespace.
+# Tests can use this as a parent reference for routes that target InferencePools.
+# Using a simple echo server instead of an actual model server to simplify the test  
+# execution, this design may need to be revised based on the test case needs. 
+apiVersion: gateway.networking.k8s.io/v1 # Using v1 as per latest Gateway API standard
+kind: Gateway
+metadata:
+  name: same-namespace
+  namespace: gateway-conformance-infra
+spec:
+  # The conformance suite runner will replace this placeholder
+  # with the actual GatewayClass name provided via flags.
+  gatewayClassName: "{GATEWAY_CLASS_NAME}"
+  listeners:
+  - name: http # Standard listener name
+    port: 80
+    protocol: HTTP
+    allowedRoutes:
+      namespaces:
+        from: Same # Restrict to same namespace initially for simplicity
+      kinds:
+      # Allows HTTPRoutes to attach, which can then reference InferencePools.
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
diff --git a/conformance/utils/assertions.go b/conformance/utils/assertions.go
new file mode 100644
index 00000000..c77d0fc5
--- /dev/null
+++ b/conformance/utils/assertions.go
@@ -0,0 +1,25 @@
+/*
+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 assertions contains custom assertion helper functions used within
+// the Gateway API Inference Extension conformance test suite.
+package assertions
+
+// TODO: Implement custom assertion functions specific to Inference Extension testing.
+// Examples might include:
+// - Asserting specific fields or structures within an inference API response body.
+// - Asserting specific metrics reported by mock model servers or EPPs.
+// - Asserting specific conditions or status fields unique to InferencePool or InferenceModel.
diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go
new file mode 100644
index 00000000..3d517863
--- /dev/null
+++ b/conformance/utils/kubernetes/helpers.go
@@ -0,0 +1,49 @@
+/*
+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 kubernetes contains helper functions for interacting with
+// Kubernetes objects within the conformance test suite.
+package kubernetes
+
+import (
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	// Import necessary utilities from the core Gateway API conformance suite
+	"sigs.k8s.io/gateway-api/conformance/utils/config"
+)
+
+// InferencePoolMustHaveCondition waits for the specified InferencePool resource
+// to exist and report the expected status condition.
+// This is a placeholder and needs full implementation.
+//
+// TODO: Implement the actual logic for this helper function.
+// It should fetch the InferencePool using the provided client and check its
+// Status.Conditions field, polling until the condition is met or a timeout occurs.
+// like HTTPRouteMustHaveCondition.
+func InferencePoolMustHaveCondition(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, poolNN types.NamespacedName, expectedCondition metav1.Condition) {
+	t.Helper() // Marks this function as a test helper
+
+	// Placeholder implementation: Log and skip the check.
+	t.Logf("Verification for InferencePool condition (%s=%s) on %s - Placeholder: Skipping check.",
+		expectedCondition.Type, expectedCondition.Status, poolNN.String())
+
+	// Skip the test using this helper until it's fully implemented.
+	t.Skip("InferencePoolMustHaveCondition helper not yet implemented")
+}
diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go
new file mode 100644
index 00000000..4f13f980
--- /dev/null
+++ b/conformance/utils/traffic/traffic.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 traffic contains helper functions specifically for generating,
+// sending, and validating network traffic related to inference workloads
+// within the Gateway API Inference Extension conformance tests.
+package traffic
+
+// TODO: Add helpers for specific inference protocols or request patterns as needed.
diff --git a/go.mod b/go.mod
index fcfb60af..30d0487e 100644
--- a/go.mod
+++ b/go.mod
@@ -17,15 +17,16 @@ require (
 	go.uber.org/zap v1.27.0
 	google.golang.org/grpc v1.71.1
 	google.golang.org/protobuf v1.36.6
-	k8s.io/api v0.32.3
-	k8s.io/apiextensions-apiserver v0.32.3
-	k8s.io/apimachinery v0.32.3
-	k8s.io/client-go v0.32.3
-	k8s.io/code-generator v0.32.3
-	k8s.io/component-base v0.32.3
+	k8s.io/api v0.32.4
+	k8s.io/apiextensions-apiserver v0.32.4
+	k8s.io/apimachinery v0.32.4
+	k8s.io/client-go v0.32.4
+	k8s.io/code-generator v0.32.4
+	k8s.io/component-base v0.32.4
 	k8s.io/utils v0.0.0-20241210054802-24370beab758
 	sigs.k8s.io/controller-runtime v0.20.4
-	sigs.k8s.io/structured-merge-diff/v4 v4.7.0
+	sigs.k8s.io/gateway-api v1.2.1
+	sigs.k8s.io/structured-merge-diff/v4 v4.6.0
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -42,17 +43,17 @@ require (
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
-	github.com/fatih/color v1.16.0 // indirect
+	github.com/fatih/color v1.17.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
-	github.com/go-openapi/jsonreference v0.20.2 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.0 // indirect
@@ -67,10 +68,10 @@ require (
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/gorilla/websocket v1.5.1 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
 	github.com/huandu/xstrings v1.3.3 // indirect
-	github.com/imdario/mergo v0.3.11 // indirect
+	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -123,11 +124,11 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiserver v0.32.3 // indirect
+	k8s.io/apiserver v0.32.4 // indirect
 	k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
-	sigs.k8s.io/controller-tools v0.14.0 // indirect
+	sigs.k8s.io/controller-tools v0.16.3 // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 )
diff --git a/go.sum b/go.sum
index b2c05a61..6688c578 100644
--- a/go.sum
+++ b/go.sum
@@ -23,25 +23,24 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q=
 github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/elastic/crd-ref-docs v0.1.0 h1:Cr5kz89QB3Iuuj7dhAfLMApCrChEGAaIBTxGk/xuRKw=
 github.com/elastic/crd-ref-docs v0.1.0/go.mod h1:X83mMBdJt05heJUYiS3T0yJ/JkCuliuhSUNav5Gjo/U=
-github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
-github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
+github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
 github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
 github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
 github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
-github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
-github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
+github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
 github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
-github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
-github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@@ -55,12 +54,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
 github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
-github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
-github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
-github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
 github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
 github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
 github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
@@ -96,14 +93,14 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
 github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
 github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -114,11 +111,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -294,26 +288,25 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
-k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
-k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
-k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss=
-k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
-k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8=
-k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc=
-k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
-k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
-k8s.io/code-generator v0.32.3 h1:31p2TVzC9+hVdSkAFruAk3JY+iSfzrJ83Qij1yZutyw=
-k8s.io/code-generator v0.32.3/go.mod h1:+mbiYID5NLsBuqxjQTygKM/DAdKpAjvBzrJd64NU1G8=
-k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k=
-k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI=
+k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
+k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
+k8s.io/apiextensions-apiserver v0.32.4 h1:IA+CoR63UDOijR/vEpow6wQnX4V6iVpzazJBskHrpHE=
+k8s.io/apiextensions-apiserver v0.32.4/go.mod h1:Y06XO/b92H8ymOdG1HlA1submf7gIhbEDc3RjriqZOs=
+k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
+k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/apiserver v0.32.4 h1:Yf7sd/y+GOQKH1Qf6wUeayZrYXe2SKZ17Bcq7VQM5HQ=
+k8s.io/apiserver v0.32.4/go.mod h1:JFUMNtE2M5yqLZpIsgCb06SkVSW1YcxW1oyLSTfjXR8=
+k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
+k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
+k8s.io/code-generator v0.32.4 h1:d4dm/43RD6xhPBX22JgJw9JUpwTKzVR6tAxJD7pz83o=
+k8s.io/code-generator v0.32.4/go.mod h1:R0bKdIg1smtvsKvj9q7SxTeKq5X9ko6PuICCGt4yqxg=
+k8s.io/component-base v0.32.4 h1:HuF+2JVLbFS5GODLIfPCb1Td6b+G2HszJoArcWOSr5I=
+k8s.io/component-base v0.32.4/go.mod h1:10KloJEYw1keU/Xmjfy9TKJqUq7J2mYdiD1VDXoco4o=
 k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4=
 k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
@@ -326,13 +319,15 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcp
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
 sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
 sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
-sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A=
-sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc=
+sigs.k8s.io/controller-tools v0.16.3 h1:z48C5/d4jCVQQvtiSBL5MYyZ3EO2eFIOXrIKMgHVhFY=
+sigs.k8s.io/controller-tools v0.16.3/go.mod h1:AEj6k+w1kYpLZv2einOH3mj52ips4W/6FUjnB5tkJGs=
+sigs.k8s.io/gateway-api v1.2.1 h1:fZZ/+RyRb+Y5tGkwxFKuYuSRQHu9dZtbjenblleOLHM=
+sigs.k8s.io/gateway-api v1.2.1/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016 h1:kXv6kKdoEtedwuqMmkqhbkgvYKeycVbC8+iPCP9j5kQ=
 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
-sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
-sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/pkg/bbr/handlers/server.go b/pkg/bbr/handlers/server.go
index 484b3318..eb6b93d6 100644
--- a/pkg/bbr/handlers/server.go
+++ b/pkg/bbr/handlers/server.go
@@ -28,6 +28,7 @@ import (
 	"google.golang.org/grpc/status"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
 )
 
 func NewServer(streaming bool) *Server {
@@ -74,6 +75,11 @@ func (s *Server) Process(srv extProcPb.ExternalProcessor_ProcessServer) error {
 				// If streaming and the body is not empty, then headers are handled when processing request body.
 				loggerVerbose.Info("Received headers, passing off header processing until body arrives...")
 			} else {
+				if requestId := requtil.ExtractHeaderValue(v, requtil.RequestIdHeaderKey); len(requestId) > 0 {
+					logger = logger.WithValues(requtil.RequestIdHeaderKey, requestId)
+					loggerVerbose = logger.V(logutil.VERBOSE)
+					ctx = log.IntoContext(ctx, logger)
+				}
 				responses, err = s.HandleRequestHeaders(req.GetRequestHeaders())
 			}
 		case *extProcPb.ProcessingRequest_RequestBody:
diff --git a/pkg/epp/backend/metrics/fake.go b/pkg/epp/backend/metrics/fake.go
index ec97c6de..58d05026 100644
--- a/pkg/epp/backend/metrics/fake.go
+++ b/pkg/epp/backend/metrics/fake.go
@@ -24,12 +24,13 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
 // FakePodMetrics is an implementation of PodMetrics that doesn't run the async refresh loop.
 type FakePodMetrics struct {
-	Pod     *Pod
+	Pod     *backend.Pod
 	Metrics *Metrics
 }
 
@@ -37,7 +38,7 @@ func (fpm *FakePodMetrics) String() string {
 	return fmt.Sprintf("Pod: %v; Metrics: %v", fpm.GetPod(), fpm.GetMetrics())
 }
 
-func (fpm *FakePodMetrics) GetPod() *Pod {
+func (fpm *FakePodMetrics) GetPod() *backend.Pod {
 	return fpm.Pod
 }
 func (fpm *FakePodMetrics) GetMetrics() *Metrics {
@@ -55,7 +56,7 @@ type FakePodMetricsClient struct {
 	Res   map[types.NamespacedName]*Metrics
 }
 
-func (f *FakePodMetricsClient) FetchMetrics(ctx context.Context, pod *Pod, existing *Metrics, port int32) (*Metrics, error) {
+func (f *FakePodMetricsClient) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *Metrics, port int32) (*Metrics, error) {
 	f.errMu.RLock()
 	err, ok := f.Err[pod.NamespacedName]
 	f.errMu.RUnlock()
diff --git a/pkg/epp/backend/metrics/metrics.go b/pkg/epp/backend/metrics/metrics.go
index 96814b4b..4cf56179 100644
--- a/pkg/epp/backend/metrics/metrics.go
+++ b/pkg/epp/backend/metrics/metrics.go
@@ -26,6 +26,7 @@ import (
 	dto "github.com/prometheus/client_model/go"
 	"github.com/prometheus/common/expfmt"
 	"go.uber.org/multierr"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 )
 
 const (
@@ -39,15 +40,8 @@ type PodMetricsClientImpl struct {
 	MetricMapping *MetricMapping
 }
 
-// FetchMetrics fetches metrics from a given pod, clones the existing metrics object and returns an
-// updated one.
-func (p *PodMetricsClientImpl) FetchMetrics(
-	ctx context.Context,
-	pod *Pod,
-	existing *Metrics,
-	port int32,
-) (*Metrics, error) {
-
+// FetchMetrics fetches metrics from a given pod, clones the existing metrics object and returns an updated one.
+func (p *PodMetricsClientImpl) FetchMetrics(ctx context.Context, pod *backend.Pod, existing *Metrics, port int32) (*Metrics, error) {
 	// Currently the metrics endpoint is hard-coded, which works with vLLM.
 	// TODO(https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/16): Consume this from InferencePool config.
 	url := "http://" + pod.Address + ":" + strconv.Itoa(int(port)) + "/metrics"
diff --git a/pkg/epp/backend/metrics/pod_metrics.go b/pkg/epp/backend/metrics/pod_metrics.go
index 7339389a..bdeb28ba 100644
--- a/pkg/epp/backend/metrics/pod_metrics.go
+++ b/pkg/epp/backend/metrics/pod_metrics.go
@@ -27,6 +27,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
@@ -35,7 +36,7 @@ const (
 )
 
 type podMetrics struct {
-	pod      atomic.Pointer[Pod]
+	pod      atomic.Pointer[backend.Pod]
 	metrics  atomic.Pointer[Metrics]
 	pmc      PodMetricsClient
 	ds       Datastore
@@ -48,14 +49,14 @@ type podMetrics struct {
 }
 
 type PodMetricsClient interface {
-	FetchMetrics(ctx context.Context, pod *Pod, existing *Metrics, port int32) (*Metrics, error)
+	FetchMetrics(ctx context.Context, pod *backend.Pod, existing *Metrics, port int32) (*Metrics, error)
 }
 
 func (pm *podMetrics) String() string {
 	return fmt.Sprintf("Pod: %v; Metrics: %v", pm.GetPod(), pm.GetMetrics())
 }
 
-func (pm *podMetrics) GetPod() *Pod {
+func (pm *podMetrics) GetPod() *backend.Pod {
 	return pm.pod.Load()
 }
 
@@ -67,8 +68,8 @@ func (pm *podMetrics) UpdatePod(in *corev1.Pod) {
 	pm.pod.Store(toInternalPod(in))
 }
 
-func toInternalPod(in *corev1.Pod) *Pod {
-	return &Pod{
+func toInternalPod(in *corev1.Pod) *backend.Pod {
+	return &backend.Pod{
 		NamespacedName: types.NamespacedName{
 			Name:      in.Name,
 			Namespace: in.Namespace,
diff --git a/pkg/epp/backend/metrics/types.go b/pkg/epp/backend/metrics/types.go
index 156ac3ed..4932e3ac 100644
--- a/pkg/epp/backend/metrics/types.go
+++ b/pkg/epp/backend/metrics/types.go
@@ -24,8 +24,8 @@ import (
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 )
 
 func NewPodMetricsFactory(pmc PodMetricsClient, refreshMetricsInterval time.Duration) *PodMetricsFactory {
@@ -58,38 +58,13 @@ func (f *PodMetricsFactory) NewPodMetrics(parentCtx context.Context, in *corev1.
 }
 
 type PodMetrics interface {
-	GetPod() *Pod
+	GetPod() *backend.Pod
 	GetMetrics() *Metrics
 	UpdatePod(*corev1.Pod)
 	StopRefreshLoop()
 	String() string
 }
 
-type Pod struct {
-	NamespacedName types.NamespacedName
-	Address        string
-}
-
-func (p *Pod) String() string {
-	if p == nil {
-		return ""
-	}
-	return fmt.Sprintf("%+v", *p)
-}
-
-func (p *Pod) Clone() *Pod {
-	if p == nil {
-		return nil
-	}
-	return &Pod{
-		NamespacedName: types.NamespacedName{
-			Name:      p.NamespacedName.Name,
-			Namespace: p.NamespacedName.Namespace,
-		},
-		Address: p.Address,
-	}
-}
-
 type Metrics struct {
 	// ActiveModels is a set of models(including LoRA adapters) that are currently cached to GPU.
 	ActiveModels  map[string]int
diff --git a/pkg/epp/backend/pod.go b/pkg/epp/backend/pod.go
new file mode 100644
index 00000000..a63a0a83
--- /dev/null
+++ b/pkg/epp/backend/pod.go
@@ -0,0 +1,45 @@
+/*
+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 backend
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/types"
+)
+
+type Pod struct {
+	NamespacedName types.NamespacedName
+	Address        string
+}
+
+func (p *Pod) String() string {
+	if p == nil {
+		return ""
+	}
+	return fmt.Sprintf("%+v", *p)
+}
+
+func (p *Pod) Clone() *Pod {
+	if p == nil {
+		return nil
+	}
+	return &Pod{
+		NamespacedName: types.NamespacedName{
+			Name:      p.NamespacedName.Name,
+			Namespace: p.NamespacedName.Namespace,
+		},
+		Address: p.Address,
+	}
+}
diff --git a/pkg/epp/handlers/request.go b/pkg/epp/handlers/request.go
index 8d30e543..65d082c8 100644
--- a/pkg/epp/handlers/request.go
+++ b/pkg/epp/handlers/request.go
@@ -35,17 +35,20 @@ import (
 func (s *StreamingServer) HandleRequestBody(
 	ctx context.Context,
 	reqCtx *RequestContext,
-	req *extProcPb.ProcessingRequest,
-	requestBodyMap map[string]interface{},
 ) (*RequestContext, error) {
 	var requestBodyBytes []byte
 	logger := log.FromContext(ctx)
+	requestBodyMap := reqCtx.Request.Body
 
 	// Resolve target models.
 	model, ok := requestBodyMap["model"].(string)
 	if !ok {
 		return reqCtx, errutil.Error{Code: errutil.BadRequest, Msg: "model not found in request"}
 	}
+	prompt, ok := requestBodyMap["prompt"].(string)
+	if !ok {
+		return reqCtx, errutil.Error{Code: errutil.BadRequest, Msg: "prompt not found in request"}
+	}
 
 	modelName := model
 
@@ -66,6 +69,7 @@ func (s *StreamingServer) HandleRequestBody(
 		Model:               model,
 		ResolvedTargetModel: modelName,
 		Critical:            modelObj.Spec.Criticality != nil && *modelObj.Spec.Criticality == v1alpha2.Critical,
+		Prompt:              prompt,
 	}
 	logger.V(logutil.DEBUG).Info("LLM request assembled", "request", llmReq)
 
@@ -147,6 +151,15 @@ func (s *StreamingServer) HandleRequestHeaders(ctx context.Context, reqCtx *Requ
 		}
 		endpoint := pod.Address + ":" + strconv.Itoa(int(pool.Spec.TargetPortNumber))
 		s.populateRequestHeaderResponse(reqCtx, endpoint, 0)
+		return nil
+	}
+
+	for _, header := range req.RequestHeaders.Headers.Headers {
+		if header.RawValue != nil {
+			reqCtx.Request.Headers[header.Key] = string(header.RawValue)
+		} else {
+			reqCtx.Request.Headers[header.Key] = header.Value
+		}
 	}
 	return nil
 }
diff --git a/pkg/epp/handlers/server.go b/pkg/epp/handlers/server.go
index 5e23c7a0..711d8cee 100644
--- a/pkg/epp/handlers/server.go
+++ b/pkg/epp/handlers/server.go
@@ -34,12 +34,13 @@ import (
 	"google.golang.org/protobuf/types/known/structpb"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
-	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"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"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
 )
 
 func NewStreamingServer(scheduler Scheduler, destinationEndpointHintMetadataNamespace, destinationEndpointHintKey string, datastore datastore.Datastore) *StreamingServer {
@@ -82,6 +83,7 @@ type RequestContext struct {
 	ResponseComplete          bool
 	ResponseStatusCode        string
 	RequestRunning            bool
+	Request                   *Request
 
 	RequestState         StreamRequestState
 	modelServerStreaming bool
@@ -95,6 +97,10 @@ type RequestContext struct {
 	respTrailerResp *extProcPb.ProcessingResponse
 }
 
+type Request struct {
+	Headers map[string]string
+	Body    map[string]interface{}
+}
 type StreamRequestState int
 
 const (
@@ -118,10 +124,14 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 	// See https://github.com/envoyproxy/envoy/issues/17540.
 	reqCtx := &RequestContext{
 		RequestState: RequestReceived,
+		Request: &Request{
+			Headers: make(map[string]string),
+			Body:    make(map[string]interface{}),
+		},
 	}
 
 	var body []byte
-	var requestBody, responseBody map[string]interface{}
+	var responseBody map[string]interface{}
 
 	// Create error handling var as each request should only report once for
 	// error metrics. This doesn't cover the error "Cannot receive stream request" because
@@ -158,6 +168,11 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 
 		switch v := req.Request.(type) {
 		case *extProcPb.ProcessingRequest_RequestHeaders:
+			if requestId := requtil.ExtractHeaderValue(v, requtil.RequestIdHeaderKey); len(requestId) > 0 {
+				logger = logger.WithValues(requtil.RequestIdHeaderKey, requestId)
+				loggerTrace = logger.V(logutil.TRACE)
+				ctx = log.IntoContext(ctx, logger)
+			}
 			err = s.HandleRequestHeaders(ctx, reqCtx, v)
 		case *extProcPb.ProcessingRequest_RequestBody:
 			loggerTrace.Info("Incoming body chunk", "EoS", v.RequestBody.EndOfStream)
@@ -167,15 +182,17 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 			// Message is buffered, we can read and decode.
 			if v.RequestBody.EndOfStream {
 				loggerTrace.Info("decoding")
-				err = json.Unmarshal(body, &requestBody)
+				err = json.Unmarshal(body, &reqCtx.Request.Body)
 				if err != nil {
 					logger.V(logutil.DEFAULT).Error(err, "Error unmarshaling request body")
+					// TODO: short circuit and send the body back as is (this could be an envoy error), currently we drop
+					// whatever the body request would have been and send our immediate response instead.
 				}
 
 				// Body stream complete. Allocate empty slice for response to use.
 				body = []byte{}
 
-				reqCtx, err = s.HandleRequestBody(ctx, reqCtx, req, requestBody)
+				reqCtx, err = s.HandleRequestBody(ctx, reqCtx)
 				if err != nil {
 					logger.V(logutil.DEFAULT).Error(err, "Error handling body")
 				} else {
@@ -256,7 +273,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 					loggerTrace.Info("stream completed")
 					// Don't send a 500 on a response error. Just let the message passthrough and log our error for debugging purposes.
 					// We assume the body is valid JSON, err messages are not guaranteed to be json, and so capturing and sending a 500 obfuscates the response message.
-					// using the standard 'err' var will send an immediate error response back to the caller.
+					// Using the standard 'err' var will send an immediate error response back to the caller.
 					var responseErr error
 					responseErr = json.Unmarshal(body, &responseBody)
 					if responseErr != nil {
@@ -447,7 +464,7 @@ func RandomWeightedDraw(logger logr.Logger, model *v1alpha2.InferenceModel, seed
 	return ""
 }
 
-func GetRandomPod(ds datastore.Datastore) *backendmetrics.Pod {
+func GetRandomPod(ds datastore.Datastore) *backend.Pod {
 	pods := ds.PodGetAll()
 	if len(pods) == 0 {
 		return nil
diff --git a/pkg/epp/metrics/metrics.go b/pkg/epp/metrics/metrics.go
index 56dcfca8..6cc0cdb8 100644
--- a/pkg/epp/metrics/metrics.go
+++ b/pkg/epp/metrics/metrics.go
@@ -30,7 +30,12 @@ import (
 const (
 	InferenceModelComponent = "inference_model"
 	InferencePoolComponent  = "inference_pool"
-	EPPComponent            = "endpoint_picker"
+	InferenceExtension      = "inference_extension"
+)
+
+var (
+	// The git hash of the latest commit in the build.
+	CommitSHA string
 )
 
 var (
@@ -178,10 +183,22 @@ var (
 		[]string{"name"},
 	)
 
-	// Scheduler Plugin Metrics
+	// Scheduler Metrics
+	SchedulerE2ELatency = compbasemetrics.NewHistogramVec(
+		&compbasemetrics.HistogramOpts{
+			Subsystem: InferenceExtension,
+			Name:      "scheduler_e2e_duration_seconds",
+			Help:      "End-to-end scheduling latency distribution in seconds.",
+			Buckets: []float64{
+				0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
+			},
+			StabilityLevel: compbasemetrics.ALPHA,
+		},
+		[]string{},
+	)
 	SchedulerPluginProcessingLatencies = compbasemetrics.NewHistogramVec(
 		&compbasemetrics.HistogramOpts{
-			Subsystem: EPPComponent,
+			Subsystem: InferenceExtension,
 			Name:      "scheduler_plugin_duration_seconds",
 			Help:      "Scheduler plugin processing latency distribution in seconds for each plugin type and plugin name.",
 			Buckets: []float64{
@@ -191,6 +208,17 @@ var (
 		},
 		[]string{"plugin_type", "plugin_name"},
 	)
+
+	// Info Metrics
+	InferenceExtensionInfo = compbasemetrics.NewGaugeVec(
+		&compbasemetrics.GaugeOpts{
+			Subsystem:      InferenceExtension,
+			Name:           "info",
+			Help:           "General information of the current build of Inference Extension.",
+			StabilityLevel: compbasemetrics.ALPHA,
+		},
+		[]string{"commit"},
+	)
 )
 
 var registerMetrics sync.Once
@@ -213,6 +241,9 @@ func Register() {
 		legacyregistry.MustRegister(inferencePoolReadyPods)
 
 		legacyregistry.MustRegister(SchedulerPluginProcessingLatencies)
+		legacyregistry.MustRegister(SchedulerE2ELatency)
+
+		legacyregistry.MustRegister(InferenceExtensionInfo)
 	})
 }
 
@@ -315,3 +346,14 @@ func RecordinferencePoolReadyPods(name string, runningPods float64) {
 func RecordSchedulerPluginProcessingLatency(pluginType, pluginName string, duration time.Duration) {
 	SchedulerPluginProcessingLatencies.WithLabelValues(pluginType, pluginName).Observe(duration.Seconds())
 }
+
+// RecordSchedulerE2ELatency records the end-to-end scheduling latency.
+func RecordSchedulerE2ELatency(duration time.Duration) {
+	SchedulerE2ELatency.WithLabelValues().Observe(duration.Seconds())
+}
+
+func RecordInferenceExtensionInfo() {
+	if CommitSHA != "" {
+		InferenceExtensionInfo.WithLabelValues(CommitSHA).Set(1)
+	}
+}
diff --git a/pkg/epp/scheduling/config.go b/pkg/epp/scheduling/config.go
index 6c0f4be7..0c33088b 100644
--- a/pkg/epp/scheduling/config.go
+++ b/pkg/epp/scheduling/config.go
@@ -18,10 +18,30 @@ package scheduling
 
 import "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/plugins"
 
+// SchedulerConfig provides a configuration for the scheduler which includes
+// items like filters, scorers, etc that influence routing decisions.
+//
+// This is not threadsafe and the machinery here does not support dynamically
+// changing this at runtime, so this should be set once on startup and not
+// changed thereafter.
 type SchedulerConfig struct {
-	preSchedulePlugins  []plugins.PreSchedule
-	scorers             []plugins.Scorer
-	filters             []plugins.Filter
-	postSchedulePlugins []plugins.PostSchedule
-	picker              plugins.Picker
+	PreSchedulePlugins  []plugins.PreSchedule
+	Filters             []plugins.Filter
+	Scorers             map[plugins.Scorer]int // map from scorer to weight
+	Picker              plugins.Picker
+	PostSchedulePlugins []plugins.PostSchedule
+}
+
+var defPlugin = &defaultPlugin{}
+
+// When the scheduler is initialized with NewScheduler function, this config will be used as default.
+// it's possible to call NewSchedulerWithConfig to pass a different argument.
+
+// For build time plugins changes, it's recommended to change the defaultConfig variable in this file.
+var defaultConfig = &SchedulerConfig{
+	PreSchedulePlugins:  []plugins.PreSchedule{},
+	Filters:             []plugins.Filter{defPlugin},
+	Scorers:             map[plugins.Scorer]int{},
+	Picker:              defPlugin,
+	PostSchedulePlugins: []plugins.PostSchedule{},
 }
diff --git a/pkg/epp/scheduling/plugins/noop.go b/pkg/epp/scheduling/plugins/noop.go
deleted file mode 100644
index 8f50ff36..00000000
--- a/pkg/epp/scheduling/plugins/noop.go
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
-Copyright 2025 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package plugins
-
-import (
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
-)
-
-// NoopPlugin provides a default, no-operation implementation of the Plugin interface.
-// It can be embedded in other plugin implementations to avoid boilerplate code for
-// unused methods.
-type NoopPlugin struct{}
-
-func (p *NoopPlugin) Name() string { return "NoopPlugin" }
-
-func (p *NoopPlugin) PreSchedule(ctx *types.SchedulingContext) {}
-
-func (p *NoopPlugin) Filter(ctx *types.SchedulingContext, pods []types.Pod) ([]types.Pod, error) {
-	return pods, nil
-}
-
-func (p *NoopPlugin) Score(ctx *types.SchedulingContext, pod types.Pod) (float64, error) {
-	return 0.0, nil
-}
-
-func (p *NoopPlugin) PostSchedule(ctx *types.SchedulingContext, res *types.Result) {}
-
-func (p *NoopPlugin) PostResponse(ctx *types.SchedulingContext, pod types.Pod) {}
diff --git a/pkg/epp/scheduling/plugins/picker/max_score_picker.go b/pkg/epp/scheduling/plugins/picker/max_score_picker.go
new file mode 100644
index 00000000..a6d7b397
--- /dev/null
+++ b/pkg/epp/scheduling/plugins/picker/max_score_picker.go
@@ -0,0 +1,65 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package picker
+
+import (
+	"fmt"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/plugins"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+var _ plugins.Picker = &MaxScorePicker{}
+
+func NewMaxScorePicker() plugins.Picker {
+	return &MaxScorePicker{
+		random: &RandomPicker{},
+	}
+}
+
+// MaxScorePicker picks the pod with the maximum score from the list of candidates.
+type MaxScorePicker struct {
+	random *RandomPicker
+}
+
+// Name returns the name of the picker.
+func (p *MaxScorePicker) Name() string {
+	return "max_score"
+}
+
+// Pick selects the pod with the maximum score from the list of candidates.
+func (p *MaxScorePicker) Pick(ctx *types.SchedulingContext, scoredPods []*types.ScoredPod) *types.Result {
+	ctx.Logger.V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a pod with the max score from %d candidates: %+v", len(scoredPods), scoredPods))
+
+	highestScorePods := []*types.ScoredPod{}
+	maxScore := -1.0 // pods min score is 0, putting value lower than 0 in order to find at least one pod as highest
+	for _, pod := range scoredPods {
+		if pod.Score > maxScore {
+			maxScore = pod.Score
+			highestScorePods = []*types.ScoredPod{pod}
+		} else if pod.Score == maxScore {
+			highestScorePods = append(highestScorePods, pod)
+		}
+	}
+
+	if len(highestScorePods) > 1 {
+		return p.random.Pick(ctx, highestScorePods) // pick randomly from the highest score pods
+	}
+
+	return &types.Result{TargetPod: highestScorePods[0]}
+}
diff --git a/pkg/epp/scheduling/plugins/picker/random_picker.go b/pkg/epp/scheduling/plugins/picker/random_picker.go
index 850108e7..fb9f9a29 100644
--- a/pkg/epp/scheduling/plugins/picker/random_picker.go
+++ b/pkg/epp/scheduling/plugins/picker/random_picker.go
@@ -20,18 +20,22 @@ import (
 	"fmt"
 	"math/rand"
 
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
+var _ plugins.Picker = &RandomPicker{}
+
+// RandomPicker picks a random pod from the list of candidates.
 type RandomPicker struct{}
 
-func (rp *RandomPicker) Name() string {
+func (p *RandomPicker) Name() string {
 	return "random"
 }
 
-func (rp *RandomPicker) Pick(ctx *types.SchedulingContext, pods []types.Pod) *types.Result {
-	ctx.Logger.V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a random pod from %d candidates: %+v", len(pods), pods))
-	i := rand.Intn(len(pods))
-	return &types.Result{TargetPod: pods[i]}
+func (p *RandomPicker) Pick(ctx *types.SchedulingContext, scoredPods []*types.ScoredPod) *types.Result {
+	ctx.Logger.V(logutil.DEBUG).Info(fmt.Sprintf("Selecting a random pod from %d candidates: %+v", len(scoredPods), scoredPods))
+	i := rand.Intn(len(scoredPods))
+	return &types.Result{TargetPod: scoredPods[i]}
 }
diff --git a/pkg/epp/scheduling/plugins/plugins.go b/pkg/epp/scheduling/plugins/plugins.go
index 4b334803..f3412ab7 100644
--- a/pkg/epp/scheduling/plugins/plugins.go
+++ b/pkg/epp/scheduling/plugins/plugins.go
@@ -49,22 +49,23 @@ type Filter interface {
 	Filter(ctx *types.SchedulingContext, pods []types.Pod) []types.Pod
 }
 
-// Scorer defines the interface for scoring pods based on context.
+// Scorer defines the interface for scoring a list of pods based on context.
+// Scorers must score pods with a value within the range of [0,1] where 1 is the highest score.
 type Scorer interface {
 	Plugin
-	Score(ctx *types.SchedulingContext, pod types.Pod) float64
+	Score(ctx *types.SchedulingContext, pods []types.Pod) map[types.Pod]float64
 }
 
-// PostSchedule is called by the scheduler after it selects a targetPod for the request.
-type PostSchedule interface {
+// Picker picks the final pod(s) to send the request to.
+type Picker interface {
 	Plugin
-	PostSchedule(ctx *types.SchedulingContext, res *types.Result)
+	Pick(ctx *types.SchedulingContext, scoredPods []*types.ScoredPod) *types.Result
 }
 
-// Picker picks the final pod(s) to send the request to.
-type Picker interface {
+// PostSchedule is called by the scheduler after it selects a targetPod for the request.
+type PostSchedule interface {
 	Plugin
-	Pick(ctx *types.SchedulingContext, pods []types.Pod) *types.Result
+	PostSchedule(ctx *types.SchedulingContext, res *types.Result)
 }
 
 // PostResponse is called by the scheduler after a successful response was sent.
diff --git a/pkg/epp/scheduling/default_config.go b/pkg/epp/scheduling/plugins/scorer/kvcache.go
similarity index 63%
rename from pkg/epp/scheduling/default_config.go
rename to pkg/epp/scheduling/plugins/scorer/kvcache.go
index e42f1317..0877691d 100644
--- a/pkg/epp/scheduling/default_config.go
+++ b/pkg/epp/scheduling/plugins/scorer/kvcache.go
@@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package scheduling
+package scorer
 
 import (
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/plugins"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
-var defPlugin = &defaultPlugin{}
+type KVCacheScorer struct{}
 
-var defaultConfig = &SchedulerConfig{
-	preSchedulePlugins:  []plugins.PreSchedule{},
-	scorers:             []plugins.Scorer{},
-	filters:             []plugins.Filter{defPlugin},
-	postSchedulePlugins: []plugins.PostSchedule{},
-	picker:              defPlugin,
+func (ss *KVCacheScorer) Name() string {
+	return "kv-cache"
+}
+
+func (ss *KVCacheScorer) Score(ctx *types.SchedulingContext, pods []types.Pod) map[types.Pod]float64 {
+	scores := make(map[types.Pod]float64, len(pods))
+	for _, pod := range pods {
+		scores[pod] = 1 - pod.GetMetrics().KVCacheUsagePercent
+	}
+	return scores
 }
diff --git a/pkg/epp/scheduling/plugins/scorer/queue.go b/pkg/epp/scheduling/plugins/scorer/queue.go
new file mode 100644
index 00000000..3df9d414
--- /dev/null
+++ b/pkg/epp/scheduling/plugins/scorer/queue.go
@@ -0,0 +1,61 @@
+/*
+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 scorer
+
+import (
+	"math"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+)
+
+type QueueScorer struct{}
+
+func (q *QueueScorer) Name() string {
+	return "queue"
+}
+
+func (q *QueueScorer) Score(ctx *types.SchedulingContext, pods []types.Pod) map[types.Pod]float64 {
+	minQueueSize := math.MaxInt
+	maxQueueSize := math.MinInt
+
+	// Iterate through the remaining pods to find min and max
+	for _, pod := range pods {
+		queueSize := pod.GetMetrics().WaitingQueueSize
+		if queueSize < minQueueSize {
+			minQueueSize = queueSize
+		}
+		if queueSize > maxQueueSize {
+			maxQueueSize = queueSize
+		}
+	}
+
+	// podScoreFunc calculates the score based on the queue size of each pod. Longer queue gets a lower score.
+	podScoreFunc := func(pod types.Pod) float64 {
+		if maxQueueSize == minQueueSize {
+			// If all pods have the same queue size, return a neutral score
+			return 1.0
+		}
+		return float64(maxQueueSize-pod.GetMetrics().WaitingQueueSize) / float64(maxQueueSize-minQueueSize)
+	}
+
+	// Create a map to hold the scores for each pod
+	scores := make(map[types.Pod]float64, len(pods))
+	for _, pod := range pods {
+		scores[pod] = podScoreFunc(pod)
+	}
+	return scores
+}
diff --git a/pkg/epp/scheduling/scheduler.go b/pkg/epp/scheduling/scheduler.go
index 322f714f..245d0a5d 100644
--- a/pkg/epp/scheduling/scheduler.go
+++ b/pkg/epp/scheduling/scheduler.go
@@ -72,25 +72,23 @@ func NewScheduler(datastore Datastore) *Scheduler {
 }
 
 func NewSchedulerWithConfig(datastore Datastore, config *SchedulerConfig) *Scheduler {
-	scheduler := &Scheduler{
+	return &Scheduler{
 		datastore:           datastore,
-		preSchedulePlugins:  config.preSchedulePlugins,
-		scorers:             config.scorers,
-		filters:             config.filters,
-		postSchedulePlugins: config.postSchedulePlugins,
-		picker:              config.picker,
+		preSchedulePlugins:  config.PreSchedulePlugins,
+		filters:             config.Filters,
+		scorers:             config.Scorers,
+		picker:              config.Picker,
+		postSchedulePlugins: config.PostSchedulePlugins,
 	}
-
-	return scheduler
 }
 
 type Scheduler struct {
 	datastore           Datastore
 	preSchedulePlugins  []plugins.PreSchedule
 	filters             []plugins.Filter
-	scorers             []plugins.Scorer
-	postSchedulePlugins []plugins.PostSchedule
+	scorers             map[plugins.Scorer]int // map from scorer to its weight
 	picker              plugins.Picker
+	postSchedulePlugins []plugins.PostSchedule
 }
 
 type Datastore interface {
@@ -102,29 +100,31 @@ func (s *Scheduler) Schedule(ctx context.Context, req *types.LLMRequest) (*types
 	logger := log.FromContext(ctx).WithValues("request", req)
 	loggerDebug := logger.V(logutil.DEBUG)
 
+	scheduleStart := time.Now()
+	defer func() {
+		metrics.RecordSchedulerE2ELatency(time.Since(scheduleStart))
+	}()
+
 	// Snapshot pod metrics from the datastore to:
 	// 1. Reduce concurrent access to the datastore.
 	// 2. Ensure consistent data during the scheduling operation of a request.
 	sCtx := types.NewSchedulingContext(ctx, req, types.ToSchedulerPodMetrics(s.datastore.PodGetAll()))
-	loggerDebug.Info(fmt.Sprintf("Scheduling a request. Metrics: %+v", sCtx.PodsSnapshot))
+	loggerDebug.Info(fmt.Sprintf("Scheduling a request, Metrics: %+v", sCtx.PodsSnapshot))
 
 	s.runPreSchedulePlugins(sCtx)
 
 	pods := s.runFilterPlugins(sCtx)
 	if len(pods) == 0 {
-		return nil, errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: "failed to find a target pod"}
+		return nil, errutil.Error{Code: errutil.Internal, Msg: "no pods available for the given request"}
 	}
+	// if we got here, there is at least one pod to score
+	weightedScorePerPod := s.runScorerPlugins(sCtx, pods)
 
-	s.runScorerPlugins(sCtx, pods)
-
-	before := time.Now()
-	res := s.picker.Pick(sCtx, pods)
-	metrics.RecordSchedulerPluginProcessingLatency(plugins.PickerPluginType, s.picker.Name(), time.Since(before))
-	loggerDebug.Info("After running picker plugins", "result", res)
+	result := s.runPickerPlugin(sCtx, weightedScorePerPod)
 
-	s.runPostSchedulePlugins(sCtx, res)
+	s.runPostSchedulePlugins(sCtx, result)
 
-	return res, nil
+	return result, nil
 }
 
 func (s *Scheduler) runPreSchedulePlugins(ctx *types.SchedulingContext) {
@@ -136,15 +136,6 @@ func (s *Scheduler) runPreSchedulePlugins(ctx *types.SchedulingContext) {
 	}
 }
 
-func (s *Scheduler) runPostSchedulePlugins(ctx *types.SchedulingContext, res *types.Result) {
-	for _, plugin := range s.postSchedulePlugins {
-		ctx.Logger.V(logutil.DEBUG).Info("Running post-schedule plugin", "plugin", plugin.Name())
-		before := time.Now()
-		plugin.PostSchedule(ctx, res)
-		metrics.RecordSchedulerPluginProcessingLatency(plugins.PostSchedulePluginType, plugin.Name(), time.Since(before))
-	}
-}
-
 func (s *Scheduler) runFilterPlugins(ctx *types.SchedulingContext) []types.Pod {
 	loggerDebug := ctx.Logger.V(logutil.DEBUG)
 	filteredPods := ctx.PodsSnapshot
@@ -160,32 +151,60 @@ func (s *Scheduler) runFilterPlugins(ctx *types.SchedulingContext) []types.Pod {
 			break
 		}
 	}
+	loggerDebug.Info("After running filter plugins")
+
 	return filteredPods
 }
 
-func (s *Scheduler) runScorerPlugins(ctx *types.SchedulingContext, pods []types.Pod) {
+func (s *Scheduler) runScorerPlugins(ctx *types.SchedulingContext, pods []types.Pod) map[types.Pod]float64 {
 	loggerDebug := ctx.Logger.V(logutil.DEBUG)
-	loggerDebug.Info("Before running score plugins", "pods", pods)
+	loggerDebug.Info("Before running scorer plugins", "pods", pods)
+
+	weightedScorePerPod := make(map[types.Pod]float64, len(pods))
 	for _, pod := range pods {
-		score := s.runScorersForPod(ctx, pod)
-		pod.SetScore(score)
+		weightedScorePerPod[pod] = float64(0) // initialize weighted score per pod with 0 value
 	}
-	loggerDebug.Info("After running score plugins", "pods", pods)
+	// Iterate through each scorer in the chain and accumulate the weighted scores.
+	for scorer, weight := range s.scorers {
+		loggerDebug.Info("Running scorer", "scorer", scorer.Name())
+		before := time.Now()
+		scores := scorer.Score(ctx, pods)
+		metrics.RecordSchedulerPluginProcessingLatency(plugins.ScorerPluginType, scorer.Name(), time.Since(before))
+		for pod, score := range scores { // weight is relative to the sum of weights
+			weightedScorePerPod[pod] += score * float64(weight) // TODO normalize score before multiply with weight
+		}
+		loggerDebug.Info("After running scorer", "scorer", scorer.Name())
+	}
+	loggerDebug.Info("After running scorer plugins")
+
+	return weightedScorePerPod
+}
+
+func (s *Scheduler) runPickerPlugin(ctx *types.SchedulingContext, weightedScorePerPod map[types.Pod]float64) *types.Result {
+	loggerDebug := ctx.Logger.V(logutil.DEBUG)
+	scoredPods := make([]*types.ScoredPod, len(weightedScorePerPod))
+	i := 0
+	for pod, score := range weightedScorePerPod {
+		scoredPods[i] = &types.ScoredPod{Pod: pod, Score: score}
+		i++
+	}
+
+	loggerDebug.Info("Before running picker plugin", "pods", weightedScorePerPod)
+	before := time.Now()
+	result := s.picker.Pick(ctx, scoredPods)
+	metrics.RecordSchedulerPluginProcessingLatency(plugins.PickerPluginType, s.picker.Name(), time.Since(before))
+	loggerDebug.Info("After running picker plugin", "result", result)
+
+	return result
 }
 
-// Iterate through each scorer in the chain and accumulate the scores.
-func (s *Scheduler) runScorersForPod(ctx *types.SchedulingContext, pod types.Pod) float64 {
-	logger := ctx.Logger.WithValues("pod", pod.GetPod().NamespacedName).V(logutil.DEBUG)
-	score := float64(0)
-	for _, scorer := range s.scorers {
-		logger.Info("Running scorer", "scorer", scorer.Name())
+func (s *Scheduler) runPostSchedulePlugins(ctx *types.SchedulingContext, res *types.Result) {
+	for _, plugin := range s.postSchedulePlugins {
+		ctx.Logger.V(logutil.DEBUG).Info("Running post-schedule plugin", "plugin", plugin.Name())
 		before := time.Now()
-		oneScore := scorer.Score(ctx, pod)
-		metrics.RecordSchedulerPluginProcessingLatency(plugins.ScorerPluginType, scorer.Name(), time.Since(before))
-		score += oneScore
-		logger.Info("After scorer", "scorer", scorer.Name(), "score", oneScore, "total score", score)
+		plugin.PostSchedule(ctx, res)
+		metrics.RecordSchedulerPluginProcessingLatency(plugins.PostSchedulePluginType, plugin.Name(), time.Since(before))
 	}
-	return score
 }
 
 type defaultPlugin struct {
diff --git a/pkg/epp/scheduling/types/types.go b/pkg/epp/scheduling/types/types.go
index e66b5fb5..4f69fae0 100644
--- a/pkg/epp/scheduling/types/types.go
+++ b/pkg/epp/scheduling/types/types.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/go-logr/logr"
 	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 )
 
@@ -41,13 +42,16 @@ func (r *LLMRequest) String() string {
 }
 
 type Pod interface {
-	GetPod() *backendmetrics.Pod
+	GetPod() *backend.Pod
 	GetMetrics() *backendmetrics.Metrics
-	SetScore(float64)
-	Score() float64
 	String() string
 }
 
+type ScoredPod struct {
+	Pod
+	Score float64
+}
+
 // SchedulingContext holds contextual information during a scheduling operation.
 type SchedulingContext struct {
 	context.Context
@@ -63,7 +67,7 @@ func (pm *PodMetrics) String() string {
 	return fmt.Sprintf("%+v", *pm)
 }
 
-func (pm *PodMetrics) GetPod() *backendmetrics.Pod {
+func (pm *PodMetrics) GetPod() *backend.Pod {
 	return pm.Pod
 }
 
@@ -71,17 +75,8 @@ func (pm *PodMetrics) GetMetrics() *backendmetrics.Metrics {
 	return pm.Metrics
 }
 
-func (pm *PodMetrics) SetScore(score float64) {
-	pm.score = score
-}
-
-func (pm *PodMetrics) Score() float64 {
-	return pm.score
-}
-
 type PodMetrics struct {
-	score float64
-	*backendmetrics.Pod
+	*backend.Pod
 	*backendmetrics.Metrics
 }
 
diff --git a/pkg/epp/server/runserver.go b/pkg/epp/server/runserver.go
index 0c0a6a6d..687a555c 100644
--- a/pkg/epp/server/runserver.go
+++ b/pkg/epp/server/runserver.go
@@ -35,7 +35,6 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/controller"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
 )
 
 // ExtProcServerRunner provides methods to manage an external process server.
@@ -49,6 +48,7 @@ type ExtProcServerRunner struct {
 	CertPath                                 string
 	UseStreaming                             bool
 	RefreshPrometheusMetricsInterval         time.Duration
+	Scheduler                                handlers.Scheduler
 
 	// This should only be used in tests. We won't need this once we don't inject metrics in the tests.
 	// TODO:(https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/432) Cleanup
@@ -137,7 +137,7 @@ func (r *ExtProcServerRunner) AsRunnable(logger logr.Logger) manager.Runnable {
 		} else {
 			srv = grpc.NewServer()
 		}
-		extProcServer := handlers.NewStreamingServer(scheduling.NewScheduler(r.Datastore), r.DestinationEndpointHintMetadataNamespace, r.DestinationEndpointHintKey, r.Datastore)
+		extProcServer := handlers.NewStreamingServer(r.Scheduler, r.DestinationEndpointHintMetadataNamespace, r.DestinationEndpointHintKey, r.Datastore)
 		extProcPb.RegisterExternalProcessorServer(
 			srv,
 			extProcServer,
diff --git a/pkg/epp/util/env/env.go b/pkg/epp/util/env/env.go
index 11e3bde1..0c6d1c6d 100644
--- a/pkg/epp/util/env/env.go
+++ b/pkg/epp/util/env/env.go
@@ -5,26 +5,25 @@ import (
 	"strconv"
 
 	"github.com/go-logr/logr"
-	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
 // getEnvFloat gets a float64 from an environment variable with a default value
 func GetEnvFloat(key string, defaultVal float64, logger logr.Logger) float64 {
 	val, exists := os.LookupEnv(key)
 	if !exists {
-		logger.V(logutil.VERBOSE).Info("Environment variable not set, using default value",
+		logger.Info("Environment variable not set, using default value",
 			"key", key, "defaultValue", defaultVal)
 		return defaultVal
 	}
 
 	floatVal, err := strconv.ParseFloat(val, 64)
 	if err != nil {
-		logger.V(logutil.VERBOSE).Info("Failed to parse environment variable as float, using default value",
+		logger.Info("Failed to parse environment variable as float, using default value",
 			"key", key, "value", val, "error", err, "defaultValue", defaultVal)
 		return defaultVal
 	}
 
-	logger.V(logutil.VERBOSE).Info("Successfully loaded environment variable",
+	logger.Info("Successfully loaded environment variable",
 		"key", key, "value", floatVal)
 	return floatVal
 }
@@ -33,19 +32,30 @@ func GetEnvFloat(key string, defaultVal float64, logger logr.Logger) float64 {
 func GetEnvInt(key string, defaultVal int, logger logr.Logger) int {
 	val, exists := os.LookupEnv(key)
 	if !exists {
-		logger.V(logutil.VERBOSE).Info("Environment variable not set, using default value",
+		logger.Info("Environment variable not set, using default value",
 			"key", key, "defaultValue", defaultVal)
 		return defaultVal
 	}
 
 	intVal, err := strconv.Atoi(val)
 	if err != nil {
-		logger.V(logutil.VERBOSE).Info("Failed to parse environment variable as int, using default value",
+		logger.Info("Failed to parse environment variable as int, using default value",
 			"key", key, "value", val, "error", err, "defaultValue", defaultVal)
 		return defaultVal
 	}
 
-	logger.V(logutil.VERBOSE).Info("Successfully loaded environment variable",
+	logger.Info("Successfully loaded environment variable",
 		"key", key, "value", intVal)
 	return intVal
 }
+
+// GetEnvString gets a string from an environment variable with a default value
+func GetEnvString(key string, defaultVal string, logger logr.Logger) string {
+	val, exists := os.LookupEnv(key)
+	if !exists {
+		logger.Info("Environment variable not set, using default value",
+			"key", key, "defaultValue", defaultVal)
+		return defaultVal
+	}
+	return val
+}
diff --git a/pkg/epp/util/request/headers.go b/pkg/epp/util/request/headers.go
new file mode 100644
index 00000000..61239a59
--- /dev/null
+++ b/pkg/epp/util/request/headers.go
@@ -0,0 +1,24 @@
+package request
+
+import (
+	"strings"
+
+	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
+)
+
+const (
+	RequestIdHeaderKey = "x-request-id"
+)
+
+func ExtractHeaderValue(req *extProcPb.ProcessingRequest_RequestHeaders, headerKey string) string {
+	// header key should be case insensitive
+	headerKeyInLower := strings.ToLower(headerKey)
+	if req != nil && req.RequestHeaders != nil && req.RequestHeaders.Headers != nil {
+		for _, headerKv := range req.RequestHeaders.Headers.Headers {
+			if strings.ToLower(headerKv.Key) == headerKeyInLower {
+				return string(headerKv.RawValue)
+			}
+		}
+	}
+	return ""
+}
diff --git a/site-src/guides/metrics.md b/site-src/guides/metrics.md
index d16c7d47..ab3ba3fd 100644
--- a/site-src/guides/metrics.md
+++ b/site-src/guides/metrics.md
@@ -35,6 +35,8 @@ curl -i ${IP}:${PORT}/v1/completions -H 'Content-Type: application/json' -d '{
 | inference_pool_average_kv_cache_utilization  | Gauge            | The average kv cache utilization for an inference server pool.    | `name`=&lt;inference-pool-name&gt;                                                 | ALPHA       |
 | inference_pool_average_queue_size            | Gauge            | The average number of requests pending in the model server queue. | `name`=&lt;inference-pool-name&gt;                                                 | ALPHA       |
 | inference_pool_ready_pods                    | Gauge            | The number of ready pods for an inference server pool.            | `name`=&lt;inference-pool-name&gt;                                                 | ALPHA       |
+| inference_extension_info                     | Gauge            | The general information of the current build.                     | `commit`=&lt;hash-of-the-build&gt;                                                 | ALPHA       |
+
 
 ## Scrape Metrics
 
diff --git a/site-src/implementations/gateways.md b/site-src/implementations/gateways.md
index b44dca6f..950c0833 100644
--- a/site-src/implementations/gateways.md
+++ b/site-src/implementations/gateways.md
@@ -6,11 +6,13 @@ This project has several implementations that are planned or in progress:
 * [Kgateway][2]
 * [Google Kubernetes Engine][3]
 * [Istio][4]
+* [Alibaba Cloud Container Service for Kubernetes][5]
 
 [1]:#envoy-gateway
 [2]:#kgateway
 [3]:#google-kubernetes-engine
 [4]:#istio
+[5]:#alibaba-cloud-container-service-for-kubernetes
 
 ## Envoy AI Gateway
 
@@ -65,3 +67,22 @@ For service mesh users, Istio also fully supports east-west (including [GAMMA](h
 
 Gateway API Inference Extension support is being tracked by this [GitHub
 Issue](https://github.com/istio/istio/issues/55768).
+
+## Alibaba Cloud Container Service for Kubernetes
+
+[Alibaba Cloud Container Service for Kubernetes (ACK)][ack] is a managed Kubernetes platform 
+offered by Alibaba Cloud. The implementation of the Gateway API in ACK is through the 
+[ACK Gateway with Inference Extension][ack-gie] component, which introduces model-aware, 
+GPU-efficient load balancing for AI workloads beyond basic HTTP routing.
+
+The ACK Gateway with Inference Extension implements the Gateway API Inference Extension 
+and provides optimized routing for serving generative AI workloads, 
+including weighted traffic splitting, mirroring, advanced routing, etc. 
+See the docs for the [usage][ack-gie-usage].
+
+Progress towards supporting Gateway API Inference Extension is being tracked 
+by [this Issue](https://github.com/AliyunContainerService/ack-gateway-api/issues/1).
+
+[ack]:https://www.alibabacloud.com/help/en/ack
+[ack-gie]:https://www.alibabacloud.com/help/en/ack/product-overview/ack-gateway-with-inference-extension
+[ack-gie-usage]:https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/intelligent-routing-and-traffic-management-with-ack-gateway-inference-extension
\ No newline at end of file
diff --git a/site-src/index.md b/site-src/index.md
index 04d1fadb..61bece27 100644
--- a/site-src/index.md
+++ b/site-src/index.md
@@ -91,7 +91,7 @@ This project is being driven by
 [WG-Serving](https://github.com/kubernetes/community/tree/master/wg-serving)
 [SIG-Network](https://github.com/kubernetes/community/tree/master/sig-network)
 to improve and standardize routing to inference workloads in Kubernetes. Check
-out the [implementations reference](implementations.md) to see the latest
+out the [implementations reference](implementations/gateways.md) to see the latest
 projects & products that support this project. If you are interested in
 contributing to or building an implementation using Gateway API then don’t
 hesitate to [get involved!](/contributing)



Test patch:
diff --git a/conformance/conformance_test.go b/conformance/conformance_test.go
new file mode 100644
index 00000000..de82d5ec
--- /dev/null
+++ b/conformance/conformance_test.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package conformance
+
+import (
+	"testing"
+)
+
+// TestConformance is the top-level function that runs the conformance tests.
+// It calls the RunConformance function which sets up the suite and executes
+// the registered tests.
+func TestConformance(t *testing.T) {
+	// RunConformance is defined in conformance.go
+	RunConformance(t)
+}
diff --git a/conformance/tests/basic/inferencepool_accepted.go b/conformance/tests/basic/inferencepool_accepted.go
new file mode 100644
index 00000000..eae59404
--- /dev/null
+++ b/conformance/tests/basic/inferencepool_accepted.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package basic
+
+import (
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // For standard condition types
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+	"sigs.k8s.io/gateway-api/pkg/features" // For standard feature names
+
+	// Import the tests package to append to ConformanceTests
+	"sigs.k8s.io/gateway-api-inference-extension/conformance/tests"
+	infrakubernetes "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes"
+)
+
+func init() {
+	// Register the InferencePoolAccepted test case with the conformance suite.
+	// This ensures it will be discovered and run by the test runner.
+	tests.ConformanceTests = append(tests.ConformanceTests, InferencePoolAccepted)
+}
+
+// InferencePoolAccepted defines the test case for verifying basic InferencePool acceptance.
+var InferencePoolAccepted = suite.ConformanceTest{
+	ShortName:   "InferencePoolAccepted",
+	Description: "A minimal InferencePool resource should be accepted by the controller and report an Accepted condition",
+	Manifests:   []string{"tests/basic/inferencepool_accepted.yaml"},
+	Features:    []features.FeatureName{},
+	Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
+		// created by the associated manifest file.
+		poolNN := types.NamespacedName{Name: "inferencepool-basic-accepted", Namespace: "gateway-conformance-app-backend"}
+
+		t.Run("InferencePool should have Accepted condition set to True", func(t *testing.T) {
+			// Define the expected status condition. We use the standard "Accepted"
+			// condition type from the Gateway API for consistency.
+			acceptedCondition := metav1.Condition{
+				Type:   string(gatewayv1.GatewayConditionAccepted), // Standard condition type
+				Status: metav1.ConditionTrue,
+				Reason: "", // "" means we don't strictly check the Reason for this basic test.
+			}
+			infrakubernetes.InferencePoolMustHaveCondition(t, s.Client, s.TimeoutConfig, poolNN, acceptedCondition)
+		})
+	},
+}
diff --git a/conformance/tests/basic/inferencepool_accepted.yaml b/conformance/tests/basic/inferencepool_accepted.yaml
new file mode 100644
index 00000000..8ae327d8
--- /dev/null
+++ b/conformance/tests/basic/inferencepool_accepted.yaml
@@ -0,0 +1,27 @@
+# Basic InferencePool for acceptance testing.
+# This manifest defines the minimal required fields to create a valid
+# InferencePool resource, which the InferencePoolAccepted test will use
+# to verify that the controller recognizes and accepts the resource.
+
+apiVersion: inference.networking.x-k8s.io/v1alpha2
+kind: InferencePool
+metadata:
+  # This name must match the 'poolNN' variable defined in the
+  # conformance/tests/basic/inferencepool_accepted.go test file.
+  name: inferencepool-basic-accepted
+  # This namespace should be one created by the base manifests.
+  namespace: gateway-conformance-app-backend
+spec:
+  # --- Selector (Required) ---
+  # Selects the Pods belonging to this pool.
+  selector:
+    app: "infra-backend-v1"
+
+  # --- Target Port (Required) ---
+  # The port the model server container listens on.
+  targetPortNumber: 3000
+
+  # --- Extension Reference ---
+  # GKE-specific configuration reference.
+  extensionRef:
+    name: infra-backend-v1-epp
diff --git a/conformance/tests/main.go b/conformance/tests/main.go
new file mode 100644
index 00000000..fc66c765
--- /dev/null
+++ b/conformance/tests/main.go
@@ -0,0 +1,35 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package tests is the root package for all Gateway API Inference Extension
+// conformance test implementations.
+package tests
+
+import (
+	// Importing the suite package to access the ConformanceTest struct definition.
+	// For initial version directly importing from the core gateway-api repo.
+	// This may be adjusted in the future if we have need to create a copy of
+	// the suite utilities.
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+	// Do NOT add blank imports for specific test packages here.
+	// They should be added to the main conformance package instead
+	// to avoid import cycles.
+)
+
+// ConformanceTests holds all the conformance tests definitions for the
+// Gateway API Inference Extension suite. Tests are registered from other packages
+// using init() functions like the one in the basic package.
+var ConformanceTests []suite.ConformanceTest
diff --git a/pkg/epp/backend/metrics/metrics_test.go b/pkg/epp/backend/metrics/metrics_test.go
index e3b45b94..53127010 100644
--- a/pkg/epp/backend/metrics/metrics_test.go
+++ b/pkg/epp/backend/metrics/metrics_test.go
@@ -30,6 +30,7 @@ import (
 	"google.golang.org/protobuf/proto"
 	"k8s.io/apimachinery/pkg/types"
 
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
@@ -486,7 +487,7 @@ func TestPromToPodMetrics(t *testing.T) {
 // there's no server running on the specified port.
 func TestFetchMetrics(t *testing.T) {
 	ctx := logutil.NewTestLoggerIntoContext(context.Background())
-	pod := &Pod{
+	pod := &backend.Pod{
 		Address: "127.0.0.1",
 		NamespacedName: types.NamespacedName{
 			Namespace: "test",
diff --git a/pkg/epp/handlers/streamingserver_test.go b/pkg/epp/handlers/server_test.go
similarity index 100%
rename from pkg/epp/handlers/streamingserver_test.go
rename to pkg/epp/handlers/server_test.go
diff --git a/pkg/epp/metrics/metrics_test.go b/pkg/epp/metrics/metrics_test.go
index 81797e6d..a2311517 100644
--- a/pkg/epp/metrics/metrics_test.go
+++ b/pkg/epp/metrics/metrics_test.go
@@ -614,7 +614,50 @@ func TestSchedulerPluginProcessingLatencies(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
-			if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, wantPluginLatencies, "endpoint_picker_scheduler_plugin_processing_latencies"); err != nil {
+			if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, wantPluginLatencies, "inference_extension_scheduler_plugin_duration_seconds"); err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
+
+func TestSchedulerE2ELatency(t *testing.T) {
+	scenarios := []struct {
+		name      string
+		durations []time.Duration
+	}{
+		{
+			name: "multiple scheduling latencies",
+			durations: []time.Duration{
+				200 * time.Microsecond,  // 0.00014s - should go in the 0.0002 bucket
+				800 * time.Microsecond,  // 0.0008s - should go in the 0.001 bucket
+				1500 * time.Microsecond, // 0.0015s - should go in the 0.002 bucket
+				3 * time.Millisecond,    // 0.003s - should go in the 0.005 bucket
+				8 * time.Millisecond,    // 0.008s - should go in the 0.01 bucket
+				15 * time.Millisecond,   // 0.015s - should go in the 0.02 bucket
+				30 * time.Millisecond,   // 0.03s - should go in the 0.05 bucket
+				75 * time.Millisecond,   // 0.075s - should go in the 0.1 bucket
+				150 * time.Millisecond,  // 0.15s - should go in the +Inf bucket
+			},
+		},
+	}
+	Register()
+	for _, scenario := range scenarios {
+		t.Run(scenario.name, func(t *testing.T) {
+			for _, duration := range scenario.durations {
+				RecordSchedulerE2ELatency(duration)
+			}
+
+			wantE2ELatency, err := os.Open("testdata/scheduler_e2e_duration_seconds_metric")
+			defer func() {
+				if err := wantE2ELatency.Close(); err != nil {
+					t.Error(err)
+				}
+			}()
+			if err != nil {
+				t.Fatal(err)
+			}
+			if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, wantE2ELatency, "inference_extension_scheduler_e2e_duration_seconds"); err != nil {
 				t.Error(err)
 			}
 		})
diff --git a/pkg/epp/metrics/testdata/scheduler_e2e_duration_seconds_metric b/pkg/epp/metrics/testdata/scheduler_e2e_duration_seconds_metric
new file mode 100644
index 00000000..0bbb35b1
--- /dev/null
+++ b/pkg/epp/metrics/testdata/scheduler_e2e_duration_seconds_metric
@@ -0,0 +1,15 @@
+# HELP inference_extension_scheduler_e2e_duration_seconds [ALPHA] End-to-end scheduling latency distribution in seconds.
+# TYPE inference_extension_scheduler_e2e_duration_seconds histogram
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.0001"} 0
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.0002"} 1
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.0005"} 1
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.001"} 2
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.002"} 3
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.005"} 4
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.01"} 5
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.02"} 6
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.05"} 7
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="0.1"} 8
+inference_extension_scheduler_e2e_duration_seconds_bucket{le="+Inf"} 9
+inference_extension_scheduler_e2e_duration_seconds_sum{} 0.2835
+inference_extension_scheduler_e2e_duration_seconds_count{} 9
diff --git a/pkg/epp/metrics/testdata/scheduler_plugin_processing_latencies_metric b/pkg/epp/metrics/testdata/scheduler_plugin_processing_latencies_metric
index 8c11757f..669d64da 100644
--- a/pkg/epp/metrics/testdata/scheduler_plugin_processing_latencies_metric
+++ b/pkg/epp/metrics/testdata/scheduler_plugin_processing_latencies_metric
@@ -1,67 +1,67 @@
-# HELP endpoint_picker_scheduler_plugin_duration_seconds [ALPHA] Scheduler plugin processing latency distribution in seconds for each plugin type and plugin name.
-# TYPE endpoint_picker_scheduler_plugin_duration_seconds histogram
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.0001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.0002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.0005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.01"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.02"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.05"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.1"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="+Inf"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_sum{plugin_name="PluginA",plugin_type="PreSchedule"} 0.1
-endpoint_picker_scheduler_plugin_duration_seconds_count{plugin_name="PluginA",plugin_type="PreSchedule"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.0001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.0002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.0005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.01"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.02"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.05"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.1"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="+Inf"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_sum{plugin_name="PluginB",plugin_type="PostSchedule"} 0.2
-endpoint_picker_scheduler_plugin_duration_seconds_count{plugin_name="PluginB",plugin_type="PostSchedule"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.0001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.0002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.0005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.01"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.02"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.05"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.1"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="+Inf"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_sum{plugin_name="PluginC",plugin_type="Filter"} 0.05
-endpoint_picker_scheduler_plugin_duration_seconds_count{plugin_name="PluginC",plugin_type="Filter"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.0001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.0002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.0005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.001"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.002"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.005"} 0
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.01"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.02"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.05"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.1"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="+Inf"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_sum{plugin_name="PluginD",plugin_type="Scorer"} 0.01
-endpoint_picker_scheduler_plugin_duration_seconds_count{plugin_name="PluginD",plugin_type="Scorer"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.0001"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.0002"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.0005"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.001"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.002"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.005"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.01"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.02"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.05"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.1"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="+Inf"} 1
-endpoint_picker_scheduler_plugin_duration_seconds_sum{plugin_name="PluginE",plugin_type="Picker"} 1e-05
-endpoint_picker_scheduler_plugin_duration_seconds_count{plugin_name="PluginE",plugin_type="Picker"} 1
+# HELP inference_extension_scheduler_plugin_duration_seconds [ALPHA] Scheduler plugin processing latency distribution in seconds for each plugin type and plugin name.
+# TYPE inference_extension_scheduler_plugin_duration_seconds histogram
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.0001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.0002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.0005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.01"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.02"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.05"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="0.1"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginA",plugin_type="PreSchedule",le="+Inf"} 1
+inference_extension_scheduler_plugin_duration_seconds_sum{plugin_name="PluginA",plugin_type="PreSchedule"} 0.1
+inference_extension_scheduler_plugin_duration_seconds_count{plugin_name="PluginA",plugin_type="PreSchedule"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.0001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.0002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.0005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.01"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.02"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.05"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="0.1"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginB",plugin_type="PostSchedule",le="+Inf"} 1
+inference_extension_scheduler_plugin_duration_seconds_sum{plugin_name="PluginB",plugin_type="PostSchedule"} 0.2
+inference_extension_scheduler_plugin_duration_seconds_count{plugin_name="PluginB",plugin_type="PostSchedule"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.0001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.0002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.0005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.01"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.02"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.05"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="0.1"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginC",plugin_type="Filter",le="+Inf"} 1
+inference_extension_scheduler_plugin_duration_seconds_sum{plugin_name="PluginC",plugin_type="Filter"} 0.05
+inference_extension_scheduler_plugin_duration_seconds_count{plugin_name="PluginC",plugin_type="Filter"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.0001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.0002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.0005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.001"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.002"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.005"} 0
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.01"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.02"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.05"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="0.1"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginD",plugin_type="Scorer",le="+Inf"} 1
+inference_extension_scheduler_plugin_duration_seconds_sum{plugin_name="PluginD",plugin_type="Scorer"} 0.01
+inference_extension_scheduler_plugin_duration_seconds_count{plugin_name="PluginD",plugin_type="Scorer"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.0001"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.0002"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.0005"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.001"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.002"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.005"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.01"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.02"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.05"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="0.1"} 1
+inference_extension_scheduler_plugin_duration_seconds_bucket{plugin_name="PluginE",plugin_type="Picker",le="+Inf"} 1
+inference_extension_scheduler_plugin_duration_seconds_sum{plugin_name="PluginE",plugin_type="Picker"} 1e-05
+inference_extension_scheduler_plugin_duration_seconds_count{plugin_name="PluginE",plugin_type="Picker"} 1
diff --git a/pkg/epp/scheduling/plugins/filter/filter_test.go b/pkg/epp/scheduling/plugins/filter/filter_test.go
index 56cccb3b..2354c3ef 100644
--- a/pkg/epp/scheduling/plugins/filter/filter_test.go
+++ b/pkg/epp/scheduling/plugins/filter/filter_test.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	k8stypes "k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/config"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
@@ -54,8 +55,7 @@ func TestFilter(t *testing.T) {
 			ctx := types.NewSchedulingContext(context.Background(), test.req, test.input)
 			got := test.filter.Filter(ctx, test.input)
 
-			opt := cmp.AllowUnexported(types.PodMetrics{})
-			if diff := cmp.Diff(test.output, got, opt); diff != "" {
+			if diff := cmp.Diff(test.output, got); diff != "" {
 				t.Errorf("Unexpected output (-want +got): %v", diff)
 			}
 		})
@@ -190,8 +190,7 @@ func TestFilterFunc(t *testing.T) {
 			ctx := types.NewSchedulingContext(context.Background(), test.req, test.input)
 			got := test.f(ctx, test.input)
 
-			opt := cmp.AllowUnexported(types.PodMetrics{})
-			if diff := cmp.Diff(test.output, got, opt); diff != "" {
+			if diff := cmp.Diff(test.output, got); diff != "" {
 				t.Errorf("Unexpected output (-want +got): %v", diff)
 			}
 		})
@@ -229,7 +228,7 @@ func TestLoRASoftAffinityDistribution(t *testing.T) {
 	// Test setup: One affinity pod and one available pod
 	pods := []types.Pod{
 		&types.PodMetrics{
-			Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "affinity-pod"}},
+			Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "affinity-pod"}},
 			Metrics: &backendmetrics.Metrics{
 				MaxActiveModels: 2,
 				ActiveModels: map[string]int{
@@ -238,7 +237,7 @@ func TestLoRASoftAffinityDistribution(t *testing.T) {
 			},
 		},
 		&types.PodMetrics{
-			Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "available-pod"}},
+			Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "available-pod"}},
 			Metrics: &backendmetrics.Metrics{
 				MaxActiveModels: 2,
 				ActiveModels:    map[string]int{},
diff --git a/pkg/epp/scheduling/plugins/scorer/kvcache_test.go b/pkg/epp/scheduling/plugins/scorer/kvcache_test.go
new file mode 100644
index 00000000..257a58c1
--- /dev/null
+++ b/pkg/epp/scheduling/plugins/scorer/kvcache_test.go
@@ -0,0 +1,95 @@
+/*
+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 scorer
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"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/scheduling/types"
+)
+
+func TestKvCacheScorer(t *testing.T) {
+	tests := []struct {
+		name              string
+		pods              []types.Pod
+		expectedScoresPod map[int]float64 // Map of pod index to expected score
+	}{
+		{
+			name: "Different KV cache utilization",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.8}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.5}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.0}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 0.2, // Highest KV cache usage (0.8) gets lowest score (1-0.8=0.2)
+				1: 0.5, // Medium KV cache usage (0.5) gets medium score (1-0.5=0.5)
+				2: 1.0, // No KV cache usage (0.0) gets highest score (1-0=1.0)
+			},
+		},
+		{
+			name: "Same KV cache utilization",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.6}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.6}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 0.4, // Both get same score (1-0.6=0.4)
+				1: 0.4,
+			},
+		},
+		{
+			name: "Zero KV cache utilization",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.0}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.0}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 1.0, // No KV cache usage gets highest score
+				1: 1.0,
+			},
+		},
+		{
+			name: "Full KV cache utilization",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 1.0}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{KVCacheUsagePercent: 0.5}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 0.0, // Full KV cache (1.0) gets lowest score (1-1=0)
+				1: 0.5, // Half KV cache (0.5) gets medium score (1-0.5=0.5)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := types.NewSchedulingContext(context.Background(), &types.LLMRequest{}, tt.pods)
+			scorer := &KVCacheScorer{}
+			scores := scorer.Score(ctx, tt.pods)
+
+			for i, pod := range tt.pods {
+				expectedScore := tt.expectedScoresPod[i]
+				assert.InDelta(t, expectedScore, scores[pod], 0.0001, "Pod %d should have score %f", i, expectedScore)
+			}
+		})
+	}
+}
diff --git a/pkg/epp/scheduling/plugins/scorer/queue_test.go b/pkg/epp/scheduling/plugins/scorer/queue_test.go
new file mode 100644
index 00000000..907681b2
--- /dev/null
+++ b/pkg/epp/scheduling/plugins/scorer/queue_test.go
@@ -0,0 +1,85 @@
+/*
+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 scorer
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"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/scheduling/types"
+)
+
+func TestQueueScorer(t *testing.T) {
+	tests := []struct {
+		name              string
+		pods              []types.Pod
+		expectedScoresPod map[int]float64 // Map of pod index to expected score
+	}{
+		{
+			name: "Different queue sizes",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 10}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 5}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 0}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 0.0, // Longest queue (10) gets lowest score
+				1: 0.5, // Medium queue (5) gets medium score
+				2: 1.0, // Shortest queue (0) gets highest score
+			},
+		},
+		{
+			name: "Same queue sizes",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 5}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 5}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 1.0, // When all pods have the same queue size, they get the same neutral score
+				1: 1.0,
+			},
+		},
+		{
+			name: "Zero queue sizes",
+			pods: []types.Pod{
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 0}},
+				&types.PodMetrics{Pod: &backend.Pod{}, Metrics: &backendmetrics.Metrics{WaitingQueueSize: 0}},
+			},
+			expectedScoresPod: map[int]float64{
+				0: 1.0,
+				1: 1.0,
+			},
+		},
+	}
+
+	scorer := &QueueScorer{}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := types.NewSchedulingContext(context.Background(), &types.LLMRequest{}, tt.pods)
+			scores := scorer.Score(ctx, tt.pods)
+
+			for i, pod := range tt.pods {
+				expectedScore := tt.expectedScoresPod[i]
+				assert.InDelta(t, expectedScore, scores[pod], 0.0001, "Pod %d should have score %f", i, expectedScore)
+			}
+		})
+	}
+}
diff --git a/pkg/epp/scheduling/scheduler_test.go b/pkg/epp/scheduling/scheduler_test.go
index 2fb26a86..2d773283 100644
--- a/pkg/epp/scheduling/scheduler_test.go
+++ b/pkg/epp/scheduling/scheduler_test.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	k8stypes "k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics" // Import config for thresholds
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
@@ -57,7 +58,7 @@ func TestSchedule(t *testing.T) {
 			// model being active, and has low KV cache.
 			input: []*backendmetrics.FakePodMetrics{
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    0,
 						KVCacheUsagePercent: 0.2,
@@ -69,7 +70,7 @@ func TestSchedule(t *testing.T) {
 					},
 				},
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    3,
 						KVCacheUsagePercent: 0.1,
@@ -81,7 +82,7 @@ func TestSchedule(t *testing.T) {
 					},
 				},
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    10,
 						KVCacheUsagePercent: 0.2,
@@ -93,17 +94,19 @@ func TestSchedule(t *testing.T) {
 				},
 			},
 			wantRes: &types.Result{
-				TargetPod: &types.PodMetrics{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
-					Metrics: &backendmetrics.Metrics{
-						WaitingQueueSize:    3,
-						KVCacheUsagePercent: 0.1,
-						MaxActiveModels:     2,
-						ActiveModels: map[string]int{
-							"foo":      1,
-							"critical": 1,
+				TargetPod: &types.ScoredPod{
+					Pod: &types.PodMetrics{
+						Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
+						Metrics: &backendmetrics.Metrics{
+							WaitingQueueSize:    3,
+							KVCacheUsagePercent: 0.1,
+							MaxActiveModels:     2,
+							ActiveModels: map[string]int{
+								"foo":      1,
+								"critical": 1,
+							},
+							WaitingModels: map[string]int{},
 						},
-						WaitingModels: map[string]int{},
 					},
 				},
 			},
@@ -118,7 +121,7 @@ func TestSchedule(t *testing.T) {
 			// pod1 will be picked because it has capacity for the sheddable request.
 			input: []*backendmetrics.FakePodMetrics{
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    0,
 						KVCacheUsagePercent: 0.2,
@@ -130,7 +133,7 @@ func TestSchedule(t *testing.T) {
 					},
 				},
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    3,
 						KVCacheUsagePercent: 0.1,
@@ -142,7 +145,7 @@ func TestSchedule(t *testing.T) {
 					},
 				},
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    10,
 						KVCacheUsagePercent: 0.2,
@@ -154,17 +157,19 @@ func TestSchedule(t *testing.T) {
 				},
 			},
 			wantRes: &types.Result{
-				TargetPod: &types.PodMetrics{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
-					Metrics: &backendmetrics.Metrics{
-						WaitingQueueSize:    0,
-						KVCacheUsagePercent: 0.2,
-						MaxActiveModels:     2,
-						ActiveModels: map[string]int{
-							"foo": 1,
-							"bar": 1,
+				TargetPod: &types.ScoredPod{
+					Pod: &types.PodMetrics{
+						Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
+						Metrics: &backendmetrics.Metrics{
+							WaitingQueueSize:    0,
+							KVCacheUsagePercent: 0.2,
+							MaxActiveModels:     2,
+							ActiveModels: map[string]int{
+								"foo": 1,
+								"bar": 1,
+							},
+							WaitingModels: map[string]int{},
 						},
-						WaitingModels: map[string]int{},
 					},
 				},
 			},
@@ -180,7 +185,7 @@ func TestSchedule(t *testing.T) {
 			// dropped.
 			input: []*backendmetrics.FakePodMetrics{
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    10,
 						KVCacheUsagePercent: 0.9,
@@ -192,7 +197,7 @@ func TestSchedule(t *testing.T) {
 					},
 				},
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    3,
 						KVCacheUsagePercent: 0.85,
@@ -204,7 +209,7 @@ func TestSchedule(t *testing.T) {
 					},
 				},
 				{
-					Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
+					Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}},
 					Metrics: &backendmetrics.Metrics{
 						WaitingQueueSize:    10,
 						KVCacheUsagePercent: 0.85,
@@ -220,24 +225,15 @@ func TestSchedule(t *testing.T) {
 		},
 	}
 
-	schedConfig := &SchedulerConfig{
-		preSchedulePlugins:  []plugins.PreSchedule{},
-		scorers:             []plugins.Scorer{},
-		filters:             []plugins.Filter{defPlugin},
-		postSchedulePlugins: []plugins.PostSchedule{},
-		picker:              defPlugin,
-	}
-
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			scheduler := NewSchedulerWithConfig(&fakeDataStore{pods: test.input}, schedConfig)
+			scheduler := NewScheduler(&fakeDataStore{pods: test.input})
 			got, err := scheduler.Schedule(context.Background(), test.req)
 			if test.err != (err != nil) {
 				t.Errorf("Unexpected error, got %v, want %v", err, test.err)
 			}
 
-			opt := cmp.AllowUnexported(types.PodMetrics{})
-			if diff := cmp.Diff(test.wantRes, got, opt); diff != "" {
+			if diff := cmp.Diff(test.wantRes, got); diff != "" {
 				t.Errorf("Unexpected output (-want +got): %v", diff)
 			}
 		})
@@ -275,37 +271,65 @@ func TestSchedulePlugins(t *testing.T) {
 		err            bool
 	}{
 		{
-			name: "all plugins executed successfully",
+			name: "all plugins executed successfully, all scorers with same weight",
 			config: SchedulerConfig{
-				preSchedulePlugins:  []plugins.PreSchedule{tp1, tp2},
-				filters:             []plugins.Filter{tp1, tp2},
-				scorers:             []plugins.Scorer{tp1, tp2},
-				postSchedulePlugins: []plugins.PostSchedule{tp1, tp2},
-				picker:              pickerPlugin,
+				PreSchedulePlugins: []plugins.PreSchedule{tp1, tp2},
+				Filters:            []plugins.Filter{tp1, tp2},
+				Scorers: map[plugins.Scorer]int{
+					tp1: 1,
+					tp2: 1,
+				},
+				Picker:              pickerPlugin,
+				PostSchedulePlugins: []plugins.PostSchedule{tp1, tp2},
 			},
 			input: []*backendmetrics.FakePodMetrics{
-				{Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				{Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				{Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			wantTargetPod:  k8stypes.NamespacedName{Name: "pod1"},
 			targetPodScore: 1.1,
 			numPodsToScore: 2,
 			err:            false,
 		},
+		{
+			name: "all plugins executed successfully, different scorers weights",
+			config: SchedulerConfig{
+				PreSchedulePlugins: []plugins.PreSchedule{tp1, tp2},
+				Filters:            []plugins.Filter{tp1, tp2},
+				Scorers: map[plugins.Scorer]int{
+					tp1: 60,
+					tp2: 40,
+				},
+				Picker:              pickerPlugin,
+				PostSchedulePlugins: []plugins.PostSchedule{tp1, tp2},
+			},
+			input: []*backendmetrics.FakePodMetrics{
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+			},
+			wantTargetPod:  k8stypes.NamespacedName{Name: "pod1"},
+			targetPodScore: 50,
+			numPodsToScore: 2,
+			err:            false,
+		},
 		{
 			name: "filter all",
 			config: SchedulerConfig{
-				preSchedulePlugins:  []plugins.PreSchedule{tp1, tp2},
-				filters:             []plugins.Filter{tp1, tp_filterAll},
-				scorers:             []plugins.Scorer{tp1, tp2},
-				postSchedulePlugins: []plugins.PostSchedule{tp1, tp2},
-				picker:              pickerPlugin,
+				PreSchedulePlugins: []plugins.PreSchedule{tp1, tp2},
+				Filters:            []plugins.Filter{tp1, tp_filterAll},
+				Scorers: map[plugins.Scorer]int{
+					tp1: 1,
+					tp2: 1,
+				},
+				Picker:              pickerPlugin,
+				PostSchedulePlugins: []plugins.PostSchedule{tp1, tp2},
 			},
 			input: []*backendmetrics.FakePodMetrics{
-				{Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
-				{Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
-				{Pod: &backendmetrics.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod2"}}},
+				{Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod3"}}},
 			},
 			numPodsToScore: 0,
 			err:            true, // no available pods to server after filter all
@@ -315,19 +339,19 @@ func TestSchedulePlugins(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			// Reset all plugins before each new test case.
-			for _, plugin := range test.config.preSchedulePlugins {
+			for _, plugin := range test.config.PreSchedulePlugins {
 				plugin.(*TestPlugin).reset()
 			}
-			for _, plugin := range test.config.postSchedulePlugins {
+			for _, plugin := range test.config.Filters {
 				plugin.(*TestPlugin).reset()
 			}
-			for _, plugin := range test.config.filters {
+			for plugin := range test.config.Scorers {
 				plugin.(*TestPlugin).reset()
 			}
-			for _, plugin := range test.config.scorers {
+			test.config.Picker.(*TestPlugin).reset()
+			for _, plugin := range test.config.PostSchedulePlugins {
 				plugin.(*TestPlugin).reset()
 			}
-			test.config.picker.(*TestPlugin).reset()
 
 			// Initialize the scheduler
 			scheduler := NewSchedulerWithConfig(&fakeDataStore{pods: test.input}, &test.config)
@@ -345,50 +369,56 @@ func TestSchedulePlugins(t *testing.T) {
 			}
 
 			// Validate output
-			opt := cmp.AllowUnexported(types.PodMetrics{})
 			wantPod := &types.PodMetrics{
-				Pod: &backendmetrics.Pod{NamespacedName: test.wantTargetPod},
+				Pod: &backend.Pod{NamespacedName: test.wantTargetPod},
 			}
-			wantPod.SetScore(test.targetPodScore)
 			wantRes := &types.Result{TargetPod: wantPod}
-			if diff := cmp.Diff(wantRes, got, opt); diff != "" {
+			if diff := cmp.Diff(wantRes, got); diff != "" {
 				t.Errorf("Unexpected output (-want +got): %v", diff)
 			}
 
 			// Validate plugin execution counts dynamically
-			for _, plugin := range test.config.preSchedulePlugins {
+			for _, plugin := range test.config.PreSchedulePlugins {
 				tp, _ := plugin.(*TestPlugin)
 				if tp.PreScheduleCallCount != 1 {
-					t.Errorf("Plugin %s PreSchedule() called %d times, expected 1", tp.NameRes, tp.PreScheduleCallCount)
+					t.Errorf("Plugin %s PreSchedule() called %d times, expected 1", plugin.Name(), tp.PreScheduleCallCount)
 				}
 			}
 
-			for _, plugin := range test.config.filters {
+			for _, plugin := range test.config.Filters {
 				tp, _ := plugin.(*TestPlugin)
 				if tp.FilterCallCount != 1 {
-					t.Errorf("Plugin %s Filter() called %d times, expected 1", tp.NameRes, tp.FilterCallCount)
+					t.Errorf("Plugin %s Filter() called %d times, expected 1", plugin.Name(), tp.FilterCallCount)
 				}
 			}
 
-			for _, plugin := range test.config.scorers {
+			for plugin := range test.config.Scorers {
 				tp, _ := plugin.(*TestPlugin)
-				if tp.ScoreCallCount != test.numPodsToScore {
-					t.Errorf("Plugin %s Score() called %d times, expected 1", tp.NameRes, tp.ScoreCallCount)
+				if tp.ScoreCallCount != 1 {
+					t.Errorf("Plugin %s Score() called %d times, expected 1", plugin.Name(), tp.ScoreCallCount)
 				}
-			}
-
-			for _, plugin := range test.config.postSchedulePlugins {
-				tp, _ := plugin.(*TestPlugin)
-				if tp.PostScheduleCallCount != 1 {
-					t.Errorf("Plugin %s PostSchedule() called %d times, expected 1", tp.NameRes, tp.PostScheduleCallCount)
+				if test.numPodsToScore != tp.NumOfScoredPods {
+					t.Errorf("Plugin %s Score() called with %d pods, expected %d", plugin.Name(), tp.NumOfScoredPods, test.numPodsToScore)
 				}
 			}
 
-			tp, _ := test.config.picker.(*TestPlugin)
+			tp, _ := test.config.Picker.(*TestPlugin)
+			if tp.NumOfPickerCandidates != test.numPodsToScore {
+				t.Errorf("Picker plugin %s Pick() called with %d candidates, expected %d", tp.Name(), tp.NumOfPickerCandidates, tp.NumOfScoredPods)
+			}
 			if tp.PickCallCount != 1 {
-				t.Errorf("Picker plugin %s Pick() called %d times, expected 1", tp.NameRes, tp.PickCallCount)
+				t.Errorf("Picker plugin %s Pick() called %d times, expected 1", tp.Name(), tp.PickCallCount)
+			}
+			if tp.WinnderPodScore != test.targetPodScore {
+				t.Errorf("winnder pod score %v, expected %v", tp.WinnderPodScore, test.targetPodScore)
 			}
 
+			for _, plugin := range test.config.PostSchedulePlugins {
+				tp, _ := plugin.(*TestPlugin)
+				if tp.PostScheduleCallCount != 1 {
+					t.Errorf("Plugin %s PostSchedule() called %d times, expected 1", plugin.Name(), tp.PostScheduleCallCount)
+				}
+			}
 		})
 	}
 }
@@ -409,13 +439,16 @@ func (fds *fakeDataStore) PodGetAll() []backendmetrics.PodMetrics {
 type TestPlugin struct {
 	NameRes               string
 	ScoreCallCount        int
+	NumOfScoredPods       int
 	ScoreRes              float64
 	FilterCallCount       int
 	FilterRes             []k8stypes.NamespacedName
 	PreScheduleCallCount  int
 	PostScheduleCallCount int
 	PickCallCount         int
+	NumOfPickerCandidates int
 	PickRes               k8stypes.NamespacedName
+	WinnderPodScore       float64
 }
 
 func (tp *TestPlugin) Name() string { return tp.NameRes }
@@ -427,29 +460,39 @@ func (tp *TestPlugin) PreSchedule(ctx *types.SchedulingContext) {
 func (tp *TestPlugin) Filter(ctx *types.SchedulingContext, pods []types.Pod) []types.Pod {
 	tp.FilterCallCount++
 	return findPods(ctx, tp.FilterRes...)
-}
 
-func (tp *TestPlugin) Score(ctx *types.SchedulingContext, pod types.Pod) float64 {
-	tp.ScoreCallCount++
-	return tp.ScoreRes
 }
 
-func (tp *TestPlugin) PostSchedule(ctx *types.SchedulingContext, res *types.Result) {
-	tp.PostScheduleCallCount++
+func (tp *TestPlugin) Score(ctx *types.SchedulingContext, pods []types.Pod) map[types.Pod]float64 {
+	tp.ScoreCallCount++
+	scoredPods := make(map[types.Pod]float64, len(pods))
+	for _, pod := range pods {
+		scoredPods[pod] += tp.ScoreRes
+	}
+	tp.NumOfScoredPods = len(scoredPods)
+	return scoredPods
 }
 
-func (tp *TestPlugin) Pick(ctx *types.SchedulingContext, pods []types.Pod) *types.Result {
+func (tp *TestPlugin) Pick(ctx *types.SchedulingContext, scoredPods []*types.ScoredPod) *types.Result {
 	tp.PickCallCount++
+	tp.NumOfPickerCandidates = len(scoredPods)
 	pod := findPods(ctx, tp.PickRes)[0]
+	tp.WinnderPodScore = getPodScore(scoredPods, pod)
 	return &types.Result{TargetPod: pod}
 }
 
+func (tp *TestPlugin) PostSchedule(ctx *types.SchedulingContext, res *types.Result) {
+	tp.PostScheduleCallCount++
+}
+
 func (tp *TestPlugin) reset() {
 	tp.PreScheduleCallCount = 0
 	tp.FilterCallCount = 0
 	tp.ScoreCallCount = 0
+	tp.NumOfScoredPods = 0
 	tp.PostScheduleCallCount = 0
 	tp.PickCallCount = 0
+	tp.NumOfPickerCandidates = 0
 }
 
 func findPods(ctx *types.SchedulingContext, names ...k8stypes.NamespacedName) []types.Pod {
@@ -463,3 +506,14 @@ func findPods(ctx *types.SchedulingContext, names ...k8stypes.NamespacedName) []
 	}
 	return res
 }
+
+func getPodScore(scoredPods []*types.ScoredPod, selectedPod types.Pod) float64 {
+	finalScore := 0.0
+	for _, scoredPod := range scoredPods {
+		if scoredPod.GetPod().NamespacedName.String() == selectedPod.GetPod().NamespacedName.String() {
+			finalScore = scoredPod.Score
+			break
+		}
+	}
+	return finalScore
+}
diff --git a/pkg/epp/util/env/env_test.go b/pkg/epp/util/env/env_test.go
index 02513e28..105beb28 100644
--- a/pkg/epp/util/env/env_test.go
+++ b/pkg/epp/util/env/env_test.go
@@ -142,3 +142,64 @@ func TestGetEnvInt(t *testing.T) {
 		})
 	}
 }
+
+func TestGetEnvString(t *testing.T) {
+	logger := testr.New(t)
+
+	tests := []struct {
+		name       string
+		key        string
+		value      string
+		defaultVal string
+		expected   string
+		setup      func()
+		teardown   func()
+	}{
+		{
+			name:       "env variable exists and is valid",
+			key:        "TEST_STR",
+			value:      "123",
+			defaultVal: "default",
+			expected:   "123",
+			setup: func() {
+				os.Setenv("TEST_STR", "123")
+			},
+			teardown: func() {
+				os.Unsetenv("TEST_STR")
+			},
+		},
+		{
+			name:       "env variable does not exist",
+			key:        "TEST_STR_MISSING",
+			defaultVal: "default",
+			expected:   "default",
+			setup:      func() {},
+			teardown:   func() {},
+		},
+		{
+			name:       "env variable is empty string",
+			key:        "TEST_STR_EMPTY",
+			value:      "",
+			defaultVal: "default",
+			expected:   "",
+			setup: func() {
+				os.Setenv("TEST_STR_EMPTY", "")
+			},
+			teardown: func() {
+				os.Unsetenv("TEST_STR_EMPTY")
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			tc.setup()
+			defer tc.teardown()
+
+			result := GetEnvString(tc.key, tc.defaultVal, logger.V(logutil.VERBOSE))
+			if result != tc.expected {
+				t.Errorf("GetEnvString(%s, %s) = %s, expected %s", tc.key, tc.defaultVal, result, tc.expected)
+			}
+		})
+	}
+}
diff --git a/pkg/epp/util/request/headers_test.go b/pkg/epp/util/request/headers_test.go
new file mode 100644
index 00000000..fa15b4f8
--- /dev/null
+++ b/pkg/epp/util/request/headers_test.go
@@ -0,0 +1,59 @@
+package request
+
+import (
+	"testing"
+
+	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
+)
+
+func TestExtractHeaderValue(t *testing.T) {
+	tests := []struct {
+		name     string
+		headers  []*corev3.HeaderValue
+		key      string
+		expected string
+	}{
+		{
+			name: "Exact match",
+			headers: []*corev3.HeaderValue{
+				{Key: "x-request-id", RawValue: []byte("123")},
+			},
+			key:      "x-request-id",
+			expected: "123",
+		},
+		{
+			name: "Case-insensitive match",
+			headers: []*corev3.HeaderValue{
+				{Key: "X-Request-ID", RawValue: []byte("456")},
+			},
+			key:      "x-request-id",
+			expected: "456",
+		},
+		{
+			name: "Non-existent key",
+			headers: []*corev3.HeaderValue{
+				{Key: "other-header", RawValue: []byte("abc")},
+			},
+			key:      "x-request-id",
+			expected: "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := &extProcPb.ProcessingRequest_RequestHeaders{
+				RequestHeaders: &extProcPb.HttpHeaders{
+					Headers: &corev3.HeaderMap{
+						Headers: tt.headers,
+					},
+				},
+			}
+
+			result := ExtractHeaderValue(req, tt.key)
+			if result != tt.expected {
+				t.Errorf("ExtractHeaderValue() = %v, want %v", result, tt.expected)
+			}
+		})
+	}
+}
diff --git a/test/integration/epp/hermetic_test.go b/test/integration/epp/hermetic_test.go
index 79b619fd..c63fd017 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -61,9 +61,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
 	runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
@@ -96,7 +98,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 	tests := []struct {
 		name              string
 		requests          []*extProcPb.ProcessingRequest
-		pods              map[backendmetrics.Pod]*backendmetrics.Metrics
+		pods              map[backend.Pod]*backendmetrics.Metrics
 		wantResponses     []*extProcPb.ProcessingResponse
 		wantMetrics       map[string]string
 		wantErr           bool
@@ -107,7 +109,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 			name:     "select lower queue and kv cache, no active lora",
 			requests: integrationutils.GenerateStreamedRequestSet(logger, "test1", "my-model"),
 			// pod-1 will be picked because it has relatively low queue size and low KV cache.
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    3,
 					KVCacheUsagePercent: 0.2,
@@ -182,7 +184,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 			requests: integrationutils.GenerateStreamedRequestSet(logger, "test2", "sql-lora"),
 			// pod-1 will be picked because it has relatively low queue size, with the requested
 			// model being active, and has low KV cache.
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    0,
 					KVCacheUsagePercent: 0.2,
@@ -267,7 +269,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 			// pod-2 will be picked despite it NOT having the requested model being active
 			// as it's above the affinity for queue size. Also is critical, so we should
 			// still honor request despite all queues > 5
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    10,
 					KVCacheUsagePercent: 0.2,
@@ -350,7 +352,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 			requests: integrationutils.GenerateStreamedRequestSet(logger, "test4", "sql-lora-sheddable"),
 			// no pods will be picked as all models are either above kv threshold,
 			// queue threshold, or both.
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    6,
 					KVCacheUsagePercent: 0.2,
@@ -398,7 +400,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 			name:     "noncritical, but one server has capacity, do not shed",
 			requests: integrationutils.GenerateStreamedRequestSet(logger, "test5", "sql-lora-sheddable"),
 			// pod 0 will be picked as all other models are above threshold
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    4,
 					KVCacheUsagePercent: 0.2,
@@ -509,7 +511,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 
 			//
 			// pod 0 will be picked as all other models are above threshold
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    4,
 					KVCacheUsagePercent: 0.2,
@@ -620,7 +622,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 
 			//
 			// pod 0 will be picked as all other models are above threshold
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    4,
 					KVCacheUsagePercent: 0.2,
@@ -732,7 +734,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 
 			//
 			// pod 0 will be picked as all other models are above threshold
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    4,
 					KVCacheUsagePercent: 0.2,
@@ -831,7 +833,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 
 			//
 			// pod 0 will be picked as all other models are above threshold
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    4,
 					KVCacheUsagePercent: 0.2,
@@ -1179,7 +1181,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 					DynamicMetadata: makeMetadata("192.168.1.1:8000"),
 				},
 			},
-			pods: map[backendmetrics.Pod]*backendmetrics.Metrics{
+			pods: map[backend.Pod]*backendmetrics.Metrics{
 				fakePod(0): {
 					WaitingQueueSize:    4,
 					KVCacheUsagePercent: 0.2,
@@ -1225,7 +1227,7 @@ func TestFullDuplexStreamed_KubeInferenceModelRequest(t *testing.T) {
 	}
 }
 
-func setUpHermeticServer(t *testing.T, podAndMetrics map[backendmetrics.Pod]*backendmetrics.Metrics, streamed bool) (client extProcPb.ExternalProcessor_ProcessClient, cleanup func()) {
+func setUpHermeticServer(t *testing.T, podAndMetrics map[backend.Pod]*backendmetrics.Metrics, streamed bool) (client extProcPb.ExternalProcessor_ProcessClient, cleanup func()) {
 	// Reconfigure the TestPodMetricsClient.
 	res := map[types.NamespacedName]*backendmetrics.Metrics{}
 	for pod, metrics := range podAndMetrics {
@@ -1303,8 +1305,8 @@ func setUpHermeticServer(t *testing.T, podAndMetrics map[backendmetrics.Pod]*bac
 	}
 }
 
-func fakePod(index int) backendmetrics.Pod {
-	return backendmetrics.Pod{
+func fakePod(index int) backend.Pod {
+	return backend.Pod{
 		NamespacedName: types.NamespacedName{Name: fmt.Sprintf("pod-%v", index), Namespace: "default"},
 		Address:        fmt.Sprintf("192.168.1.%d", index+1),
 	}
@@ -1350,6 +1352,7 @@ func BeforeSuite() func() {
 	// Adjust from defaults
 	serverRunner.PoolNamespacedName = types.NamespacedName{Name: "vllm-llama3-8b-instruct-pool", Namespace: "default"}
 	serverRunner.Datastore = datastore.NewDatastore(context.Background(), pmf)
+	serverRunner.Scheduler = scheduling.NewScheduler(serverRunner.Datastore)
 	serverRunner.SecureServing = false
 
 	if err := serverRunner.SetupWithManager(context.Background(), mgr); err != nil {