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

Issue:
[Scorer] prefix-cache-scorer benchmark panic
**What happened**:

run command:
```
go test ./pkg/... -bench=. -benchmem
```

result in panic:
```
ok      sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework        0.503s
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x8 pc=0x101af6ed8]

goroutine 38 [running]:
sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/multi/prefix.BenchmarkPrefixPluginStress(0x140001e0008)
        /Users/fraps/daocloud/github-code/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go:402 +0x228
testing.(*B).runN(0x140001e0008, 0x1)
        /usr/local/go/src/testing/benchmark.go:202 +0x198
testing.(*B).run1.func1()
        /usr/local/go/src/testing/benchmark.go:224 +0x4c
created by testing.(*B).run1 in goroutine 1
        /usr/local/go/src/testing/benchmark.go:217 +0x90
exit status 2
FAIL    sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/multi/prefix   0.459s
```

**What you expected to happen**:
All benchmark tests pass.


Hints from issue comments:

Solution patch:
diff --git a/Makefile b/Makefile
index 66b9a16..dd8fcff 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,8 @@ SHELL = /usr/bin/env bash -o pipefail
 
 GIT_COMMIT_SHA ?= "$(shell git rev-parse HEAD 2>/dev/null)"
 GIT_TAG ?= $(shell git describe --tags --dirty --always)
-PLATFORMS ?= linux/amd64
+TARGETARCH ?= $(shell go env GOARCH)
+PLATFORMS ?= linux/$(TARGETARCH)
 DOCKER_BUILDX_CMD ?= docker buildx
 IMAGE_BUILD_CMD ?= $(DOCKER_BUILDX_CMD) build
 IMAGE_BUILD_EXTRA_OPTS ?=
@@ -99,6 +100,7 @@ help: ## Display this help.
 .PHONY: generate
 generate: controller-gen code-generator ## Generate WebhookConfiguration, ClusterRole, CustomResourceDefinition objects, code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
 	$(CONTROLLER_GEN) object:headerFile="hack/boilerplate/boilerplate.generatego.txt" paths="./..."
+	$(CONTROLLER_GEN) crd output:dir="./config/crd/bases" paths="./..."
 	./hack/update-codegen.sh
 
 # Use same code-generator version as k8s.io/api
@@ -142,6 +144,10 @@ test-unit: ## Run unit tests.
 	go tool cover -func=cover.out; \
 	rm cover.out
 
+.PHONY: test-benchmark
+test-benchmark: ## Run benchmarks.
+	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./pkg/... -bench=. -benchmem;
+
 .PHONY: test-integration
 test-integration: envtest ## Run integration tests.
 	CGO_ENABLED=1 KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./test/integration/epp/... -race -coverprofile cover.out
@@ -307,13 +313,24 @@ live-docs:
 	docker build -t gaie/mkdocs hack/mkdocs/image
 	docker run --rm -it -p 3000:3000 -v ${PWD}:/docs gaie/mkdocs
 
-.PHONY: apix-ref-docs
-apix-ref-docs: crd-ref-docs
+.PHONY: api-ref-docs-all
+api-ref-docs-all: apix-v1a1-ref-docs apix-v1a2-ref-docs api-ref-docs
+
+.PHONY: apix-v1a1-ref-docs
+apix-v1a1-ref-docs: crd-ref-docs
+	${CRD_REF_DOCS} \
+		--source-path=${PWD}/apix/v1alpha1 \
+		--config=crd-ref-docs.yaml \
+		--renderer=markdown \
+		--output-path=${PWD}/site-src/reference/x-v1a1-spec.md
+
+.PHONY: apix-v1a2-ref-docs
+apix-v1a2-ref-docs: crd-ref-docs
 	${CRD_REF_DOCS} \
 		--source-path=${PWD}/apix/v1alpha2 \
 		--config=crd-ref-docs.yaml \
 		--renderer=markdown \
-		--output-path=${PWD}/site-src/reference/x-spec.md
+		--output-path=${PWD}/site-src/reference/x-v1a2-spec.md
 
 .PHONY: api-ref-docs
 api-ref-docs: crd-ref-docs
@@ -339,11 +356,11 @@ uninstall: generate kustomize ## Uninstall CRDs from the K8s cluster specified i
 
 ##@ Helm
 .PHONY: inferencepool-helm-chart-push
-inferencepool-helm-chart-push: yq helm
+inferencepool-helm-chart-push: yq helm-install
 	CHART=inferencepool EXTRA_TAG="$(EXTRA_TAG)" IMAGE_REGISTRY="$(IMAGE_REGISTRY)" YQ="$(YQ)" HELM="$(HELM)" ./hack/push-chart.sh
 
 .PHONY: bbr-helm-chart-push
-bbr-helm-chart-push: yq helm
+bbr-helm-chart-push: yq helm-install
 	CHART=body-based-routing EXTRA_TAG="$(EXTRA_TAG)" IMAGE_REGISTRY="$(IMAGE_REGISTRY)" YQ="$(YQ)" HELM="$(HELM)" ./hack/push-chart.sh
 
 ##@ Release
@@ -384,7 +401,7 @@ GCI = $(LOCALBIN)/gci
 
 ## Tool Versions
 KUSTOMIZE_VERSION ?= v5.4.3
-CONTROLLER_TOOLS_VERSION ?= v0.16.1
+CONTROLLER_TOOLS_VERSION ?= v0.19.0
 ENVTEST_VERSION ?= release-0.19
 CRD_REF_DOCS_VERSION ?= v0.2.0
 GOLANGCI_LINT_VERSION ?= v2.3.0
diff --git a/api/v1/inferencepool_types.go b/api/v1/inferencepool_types.go
index 76026df..92e7aff 100644
--- a/api/v1/inferencepool_types.go
+++ b/api/v1/inferencepool_types.go
@@ -202,8 +202,42 @@ type ParentStatus struct {
 	//
 	// +required
 	ParentRef ParentReference `json:"parentRef,omitzero"`
+
+	// ControllerName is a domain/path string that indicates the name of the controller that
+	// wrote this status. This corresponds with the GatewayClass controllerName field when the
+	// parentRef references a Gateway kind.
+	//
+	// Example: "example.net/gateway-controller".
+	//
+	// The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes names:
+	//
+	//  https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+	//
+	// Controllers MAY populate this field when writing status. When populating this field, controllers
+	// should ensure that entries to status populated with their ControllerName are cleaned up when they
+	// are no longer necessary.
+	//
+	// +optional
+	ControllerName ControllerName `json:"controllerName,omitempty"`
 }
 
+// ControllerName is the name of a controller that manages ParentStatus. It must be a domain prefixed
+// path.
+//
+// Valid values include:
+//
+// * "example.com/bar"
+//
+// Invalid values include:
+//
+// * "example.com" - must include path
+// * "foo.example.com" - must include path
+//
+// +kubebuilder:validation:MinLength=1
+// +kubebuilder:validation:MaxLength=253
+// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$`
+type ControllerName string
+
 // InferencePoolConditionType is a type of status condition for the InferencePool.
 type InferencePoolConditionType string
 
@@ -274,6 +308,38 @@ const (
 	InferencePoolReasonInvalidExtensionRef InferencePoolReason = "InvalidExtensionRef"
 )
 
+const (
+	// InferencePoolConditionExported is a type of condition that indicates whether the
+	// controller was able to export the InferencePool to the specified clusters.
+	//
+	// Possible reasons for this condition to be True are:
+	//
+	// * "Exported"
+	//
+	// Possible reasons for this condition to be False are:
+	//
+	// * "NotRequested"
+	// * "NotSupported"
+	//
+	// Controllers MAY raise this condition with other reasons, but should
+	// prefer to use the reasons listed above to improve interoperability.
+	InferencePoolConditionExported InferencePoolConditionType = "Exported"
+
+	// InferencePoolReasonExported is a reason used with the "Exported" condition when the
+	// condition is true.
+	InferencePoolReasonExported InferencePoolReason = "Exported"
+
+	// InferencePoolReasonNotRequested is a reason used with the "Exported" condition when the
+	// condition is false and no export was requested by the InferencePool. This indicates a
+	// deliberate non-action rather than an error.
+	InferencePoolReasonNotRequested InferencePoolReason = "NotRequested"
+
+	// InferencePoolReasonNotSupported is a reason used with the "Exported" condition when the
+	// condition is false and the export was requested but is not supported by the implementation.
+	// Controllers should include details in the condition message.
+	InferencePoolReasonNotSupported InferencePoolReason = "NotSupported"
+)
+
 // ParentReference identifies an API object. It is used to associate the InferencePool with a
 // parent resource, such as a Gateway.
 type ParentReference struct {
diff --git a/api/v1/shared_types.go b/api/v1/shared_types.go
index bc315fd..5d5d3e7 100644
--- a/api/v1/shared_types.go
+++ b/api/v1/shared_types.go
@@ -137,7 +137,7 @@ type LabelSelector struct {
 	// The matching logic is an AND operation on all entries.
 	//
 	// +required
-	// +kubebuilder:validation:MinItems=1
-	// +kubebuilder:validation:MaxItems=64
+	// +kubebuilder:validation:MinProperties=1
+	// +kubebuilder:validation:MaxProperties=64
 	MatchLabels map[LabelKey]LabelValue `json:"matchLabels,omitempty"`
 }
diff --git a/apix/v1alpha1/doc.go b/apix/v1alpha1/doc.go
new file mode 100644
index 0000000..122c3b9
--- /dev/null
+++ b/apix/v1alpha1/doc.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v1alpha1 contains API Schema definitions for the
+// inference.networking.x-k8s.io API group.
+//
+// +kubebuilder:object:generate=true
+// +groupName=inference.networking.x-k8s.io
+package v1alpha1
diff --git a/apix/v1alpha1/inferencepoolimport_types.go b/apix/v1alpha1/inferencepoolimport_types.go
new file mode 100644
index 0000000..2239aa3
--- /dev/null
+++ b/apix/v1alpha1/inferencepoolimport_types.go
@@ -0,0 +1,143 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+// InferencePoolImport is the Schema for the InferencePoolImports API.
+//
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:shortName=infpimp
+// +kubebuilder:subresource:status
+// +kubebuilder:storageversion
+// +genclient
+type InferencePoolImport struct {
+	metav1.TypeMeta `json:",inline"`
+
+	// +optional
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	// Status defines the observed state of the InferencePoolImport.
+	//
+	// +optional
+	//nolint:kubeapilinter // status should not be a pointer.
+	Status InferencePoolImportStatus `json:"status,omitempty"`
+}
+
+// InferencePoolImportList contains a list of InferencePoolImports.
+//
+// +kubebuilder:object:root=true
+type InferencePoolImportList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []InferencePoolImport `json:"items"`
+}
+
+// InferencePoolImportStatus defines the observed state of the InferencePoolImport.
+type InferencePoolImportStatus struct {
+	// Controllers is a list of controllers that are responsible for managing the InferencePoolImport.
+	//
+	// +listType=map
+	// +listMapKey=name
+	// +kubebuilder:validation:MaxItems=8
+	// +kubebuilder:validation:Required
+	Controllers []ImportController `json:"controllers"`
+}
+
+// ImportController defines a controller that is responsible for managing the InferencePoolImport.
+type ImportController struct {
+	// Name is a domain/path string that indicates the name of the controller that manages the
+	// InferencePoolImport. Name corresponds to the GatewayClass controllerName field when the
+	// controller will manage parents of type "Gateway". Otherwise, the name is implementation-specific.
+	//
+	// Example: "example.net/import-controller".
+	//
+	// The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes
+	// names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+	//
+	// A controller MUST populate this field when writing status and ensure that entries to status
+	// populated with their controller name are removed when they are no longer necessary.
+	//
+	// +required
+	Name ControllerName `json:"name"`
+
+	// ExportingClusters is a list of clusters that exported the InferencePool(s) that back the
+	// InferencePoolImport. Required when the controller is responsible for CRUD'ing the InferencePoolImport
+	// from the exported InferencePool(s).
+	//
+	// +optional
+	ExportingClusters []ExportingCluster `json:"exportingClusters,omitempty"`
+
+	// Parents is a list of parent resources, typically Gateways, that are associated with the
+	// InferencePoolImport, and the status of the InferencePoolImport with respect to each parent.
+	//
+	// Ancestor would be a more accurate name, but Parent is consistent with InferencePool terminology.
+	//
+	// Required when the controller manages the InferencePoolImport as an HTTPRoute backendRef. The controller
+	// must add an entry for each parent it manages and remove the parent entry when the controller no longer
+	// considers the InferencePoolImport to be associated with that parent.
+	//
+	// +optional
+	// +listType=atomic
+	Parents []v1.ParentStatus `json:"parents,omitempty"`
+
+	// Conditions track the state of the InferencePoolImport.
+	//
+	// Known condition types are:
+	//
+	//  * "Accepted"
+	//
+	// +optional
+	// +listType=map
+	// +listMapKey=type
+	// +kubebuilder:validation:MaxItems=8
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// ControllerName is the name of a controller that manages a resource. It must be a domain prefixed path.
+//
+// Valid values include:
+//
+//   - "example.com/bar"
+//
+// Invalid values include:
+//
+//   - "example.com" - must include path
+//   - "foo.example.com" - must include path
+//
+// +kubebuilder:validation:MinLength=1
+// +kubebuilder:validation:MaxLength=253
+// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$`
+type ControllerName string
+
+// ClusterName is the name of a cluster that exported the InferencePool.
+//
+// +kubebuilder:validation:MinLength=1
+// +kubebuilder:validation:MaxLength=253
+type ClusterName string
+
+// ExportingCluster defines a cluster that exported the InferencePool that backs this InferencePoolImport.
+type ExportingCluster struct {
+	// Name of the exporting cluster (must be unique within the list).
+	//
+	// +kubebuilder:validation:Required
+	Name ClusterName `json:"name"`
+}
diff --git a/apix/v1alpha1/shared_types.go b/apix/v1alpha1/shared_types.go
new file mode 100644
index 0000000..56fd71d
--- /dev/null
+++ b/apix/v1alpha1/shared_types.go
@@ -0,0 +1,24 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+// ExportAnnotationKey is the annotation key used to export an InferencePool.
+var ExportAnnotationKey = "inference.networking.x-k8s.io/export"
+
+// ExportAnnotationVal is the annotation value used to export an InferencePool
+// to all clusters.
+var ExportAnnotationVal = "ClusterSet"
diff --git a/apix/v1alpha1/zz_generated.deepcopy.go b/apix/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 0000000..f6f1826
--- /dev/null
+++ b/apix/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,156 @@
+//go:build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExportingCluster) DeepCopyInto(out *ExportingCluster) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExportingCluster.
+func (in *ExportingCluster) DeepCopy() *ExportingCluster {
+	if in == nil {
+		return nil
+	}
+	out := new(ExportingCluster)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImportController) DeepCopyInto(out *ImportController) {
+	*out = *in
+	if in.ExportingClusters != nil {
+		in, out := &in.ExportingClusters, &out.ExportingClusters
+		*out = make([]ExportingCluster, len(*in))
+		copy(*out, *in)
+	}
+	if in.Parents != nil {
+		in, out := &in.Parents, &out.Parents
+		*out = make([]v1.ParentStatus, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]metav1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImportController.
+func (in *ImportController) DeepCopy() *ImportController {
+	if in == nil {
+		return nil
+	}
+	out := new(ImportController)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InferencePoolImport) DeepCopyInto(out *InferencePoolImport) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferencePoolImport.
+func (in *InferencePoolImport) DeepCopy() *InferencePoolImport {
+	if in == nil {
+		return nil
+	}
+	out := new(InferencePoolImport)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *InferencePoolImport) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InferencePoolImportList) DeepCopyInto(out *InferencePoolImportList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]InferencePoolImport, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferencePoolImportList.
+func (in *InferencePoolImportList) DeepCopy() *InferencePoolImportList {
+	if in == nil {
+		return nil
+	}
+	out := new(InferencePoolImportList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *InferencePoolImportList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InferencePoolImportStatus) DeepCopyInto(out *InferencePoolImportStatus) {
+	*out = *in
+	if in.Controllers != nil {
+		in, out := &in.Controllers, &out.Controllers
+		*out = make([]ImportController, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferencePoolImportStatus.
+func (in *InferencePoolImportStatus) DeepCopy() *InferencePoolImportStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(InferencePoolImportStatus)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/apix/v1alpha1/zz_generated.register.go b/apix/v1alpha1/zz_generated.register.go
new file mode 100644
index 0000000..4894b76
--- /dev/null
+++ b/apix/v1alpha1/zz_generated.register.go
@@ -0,0 +1,70 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by register-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// GroupName specifies the group name used to register the objects.
+const GroupName = "inference.networking.x-k8s.io"
+
+// GroupVersion specifies the group and the version used to register the objects.
+var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1alpha1"}
+
+// SchemeGroupVersion is group version used to register these objects
+// Deprecated: use GroupVersion instead.
+var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
+
+// Resource takes an unqualified resource and returns a Group qualified GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+var (
+	// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
+	SchemeBuilder      runtime.SchemeBuilder
+	localSchemeBuilder = &SchemeBuilder
+	// Deprecated: use Install instead
+	AddToScheme = localSchemeBuilder.AddToScheme
+	Install     = localSchemeBuilder.AddToScheme
+)
+
+func init() {
+	// We only register manually written functions here. The registration of the
+	// generated functions takes place in the generated files. The separation
+	// makes the code compile even when the generated files are missing.
+	localSchemeBuilder.Register(addKnownTypes)
+}
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&InferencePoolImport{},
+		&InferencePoolImportList{},
+	)
+	// AddToGroupVersion allows the serialization of client types like ListOptions.
+	v1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/client-go/applyconfiguration/api/v1/parentstatus.go b/client-go/applyconfiguration/api/v1/parentstatus.go
index 5accb0f..929df58 100644
--- a/client-go/applyconfiguration/api/v1/parentstatus.go
+++ b/client-go/applyconfiguration/api/v1/parentstatus.go
@@ -20,13 +20,15 @@ package v1
 
 import (
 	metav1 "k8s.io/client-go/applyconfigurations/meta/v1"
+	apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 )
 
 // ParentStatusApplyConfiguration represents a declarative configuration of the ParentStatus type for use
 // with apply.
 type ParentStatusApplyConfiguration struct {
-	Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"`
-	ParentRef  *ParentReferenceApplyConfiguration   `json:"parentRef,omitempty"`
+	Conditions     []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"`
+	ParentRef      *ParentReferenceApplyConfiguration   `json:"parentRef,omitempty"`
+	ControllerName *apiv1.ControllerName                `json:"controllerName,omitempty"`
 }
 
 // ParentStatusApplyConfiguration constructs a declarative configuration of the ParentStatus type for use with
@@ -55,3 +57,11 @@ func (b *ParentStatusApplyConfiguration) WithParentRef(value *ParentReferenceApp
 	b.ParentRef = value
 	return b
 }
+
+// WithControllerName sets the ControllerName field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ControllerName field is set to the value of the last call.
+func (b *ParentStatusApplyConfiguration) WithControllerName(value apiv1.ControllerName) *ParentStatusApplyConfiguration {
+	b.ControllerName = &value
+	return b
+}
diff --git a/client-go/applyconfiguration/apix/v1alpha1/exportingcluster.go b/client-go/applyconfiguration/apix/v1alpha1/exportingcluster.go
new file mode 100644
index 0000000..d1b2db0
--- /dev/null
+++ b/client-go/applyconfiguration/apix/v1alpha1/exportingcluster.go
@@ -0,0 +1,43 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+)
+
+// ExportingClusterApplyConfiguration represents a declarative configuration of the ExportingCluster type for use
+// with apply.
+type ExportingClusterApplyConfiguration struct {
+	Name *apixv1alpha1.ClusterName `json:"name,omitempty"`
+}
+
+// ExportingClusterApplyConfiguration constructs a declarative configuration of the ExportingCluster type for use with
+// apply.
+func ExportingCluster() *ExportingClusterApplyConfiguration {
+	return &ExportingClusterApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *ExportingClusterApplyConfiguration) WithName(value apixv1alpha1.ClusterName) *ExportingClusterApplyConfiguration {
+	b.Name = &value
+	return b
+}
diff --git a/client-go/applyconfiguration/apix/v1alpha1/importcontroller.go b/client-go/applyconfiguration/apix/v1alpha1/importcontroller.go
new file mode 100644
index 0000000..1c30787
--- /dev/null
+++ b/client-go/applyconfiguration/apix/v1alpha1/importcontroller.go
@@ -0,0 +1,87 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/client-go/applyconfigurations/meta/v1"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+	v1 "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/api/v1"
+)
+
+// ImportControllerApplyConfiguration represents a declarative configuration of the ImportController type for use
+// with apply.
+type ImportControllerApplyConfiguration struct {
+	Name              *apixv1alpha1.ControllerName         `json:"name,omitempty"`
+	ExportingClusters []ExportingClusterApplyConfiguration `json:"exportingClusters,omitempty"`
+	Parents           []v1.ParentStatusApplyConfiguration  `json:"parents,omitempty"`
+	Conditions        []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"`
+}
+
+// ImportControllerApplyConfiguration constructs a declarative configuration of the ImportController type for use with
+// apply.
+func ImportController() *ImportControllerApplyConfiguration {
+	return &ImportControllerApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *ImportControllerApplyConfiguration) WithName(value apixv1alpha1.ControllerName) *ImportControllerApplyConfiguration {
+	b.Name = &value
+	return b
+}
+
+// WithExportingClusters adds the given value to the ExportingClusters field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the ExportingClusters field.
+func (b *ImportControllerApplyConfiguration) WithExportingClusters(values ...*ExportingClusterApplyConfiguration) *ImportControllerApplyConfiguration {
+	for i := range values {
+		if values[i] == nil {
+			panic("nil value passed to WithExportingClusters")
+		}
+		b.ExportingClusters = append(b.ExportingClusters, *values[i])
+	}
+	return b
+}
+
+// WithParents adds the given value to the Parents field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Parents field.
+func (b *ImportControllerApplyConfiguration) WithParents(values ...*v1.ParentStatusApplyConfiguration) *ImportControllerApplyConfiguration {
+	for i := range values {
+		if values[i] == nil {
+			panic("nil value passed to WithParents")
+		}
+		b.Parents = append(b.Parents, *values[i])
+	}
+	return b
+}
+
+// WithConditions adds the given value to the Conditions field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Conditions field.
+func (b *ImportControllerApplyConfiguration) WithConditions(values ...*metav1.ConditionApplyConfiguration) *ImportControllerApplyConfiguration {
+	for i := range values {
+		if values[i] == nil {
+			panic("nil value passed to WithConditions")
+		}
+		b.Conditions = append(b.Conditions, *values[i])
+	}
+	return b
+}
diff --git a/client-go/applyconfiguration/apix/v1alpha1/inferencepoolimport.go b/client-go/applyconfiguration/apix/v1alpha1/inferencepoolimport.go
new file mode 100644
index 0000000..f29c038
--- /dev/null
+++ b/client-go/applyconfiguration/apix/v1alpha1/inferencepoolimport.go
@@ -0,0 +1,233 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// InferencePoolImportApplyConfiguration represents a declarative configuration of the InferencePoolImport type for use
+// with apply.
+type InferencePoolImportApplyConfiguration struct {
+	v1.TypeMetaApplyConfiguration    `json:",inline"`
+	*v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
+	Status                           *InferencePoolImportStatusApplyConfiguration `json:"status,omitempty"`
+}
+
+// InferencePoolImport constructs a declarative configuration of the InferencePoolImport type for use with
+// apply.
+func InferencePoolImport(name, namespace string) *InferencePoolImportApplyConfiguration {
+	b := &InferencePoolImportApplyConfiguration{}
+	b.WithName(name)
+	b.WithNamespace(namespace)
+	b.WithKind("InferencePoolImport")
+	b.WithAPIVersion("inference.networking.x-k8s.io/v1alpha1")
+	return b
+}
+func (b InferencePoolImportApplyConfiguration) IsApplyConfiguration() {}
+
+// WithKind sets the Kind field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Kind field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithKind(value string) *InferencePoolImportApplyConfiguration {
+	b.TypeMetaApplyConfiguration.Kind = &value
+	return b
+}
+
+// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the APIVersion field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithAPIVersion(value string) *InferencePoolImportApplyConfiguration {
+	b.TypeMetaApplyConfiguration.APIVersion = &value
+	return b
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithName(value string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.Name = &value
+	return b
+}
+
+// WithGenerateName sets the GenerateName field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GenerateName field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithGenerateName(value string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.GenerateName = &value
+	return b
+}
+
+// WithNamespace sets the Namespace field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Namespace field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithNamespace(value string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.Namespace = &value
+	return b
+}
+
+// WithUID sets the UID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UID field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithUID(value types.UID) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.UID = &value
+	return b
+}
+
+// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ResourceVersion field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithResourceVersion(value string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.ResourceVersion = &value
+	return b
+}
+
+// WithGeneration sets the Generation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Generation field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithGeneration(value int64) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.Generation = &value
+	return b
+}
+
+// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the CreationTimestamp field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithCreationTimestamp(value metav1.Time) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
+	return b
+}
+
+// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionTimestamp field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
+	return b
+}
+
+// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
+	return b
+}
+
+// WithLabels puts the entries into the Labels field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Labels field,
+// overwriting an existing map entries in Labels field with the same key.
+func (b *InferencePoolImportApplyConfiguration) WithLabels(entries map[string]string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
+		b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
+	}
+	for k, v := range entries {
+		b.ObjectMetaApplyConfiguration.Labels[k] = v
+	}
+	return b
+}
+
+// WithAnnotations puts the entries into the Annotations field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Annotations field,
+// overwriting an existing map entries in Annotations field with the same key.
+func (b *InferencePoolImportApplyConfiguration) WithAnnotations(entries map[string]string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
+		b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
+	}
+	for k, v := range entries {
+		b.ObjectMetaApplyConfiguration.Annotations[k] = v
+	}
+	return b
+}
+
+// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
+func (b *InferencePoolImportApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	for i := range values {
+		if values[i] == nil {
+			panic("nil value passed to WithOwnerReferences")
+		}
+		b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
+	}
+	return b
+}
+
+// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Finalizers field.
+func (b *InferencePoolImportApplyConfiguration) WithFinalizers(values ...string) *InferencePoolImportApplyConfiguration {
+	b.ensureObjectMetaApplyConfigurationExists()
+	for i := range values {
+		b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
+	}
+	return b
+}
+
+func (b *InferencePoolImportApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
+	if b.ObjectMetaApplyConfiguration == nil {
+		b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
+	}
+}
+
+// WithStatus sets the Status field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Status field is set to the value of the last call.
+func (b *InferencePoolImportApplyConfiguration) WithStatus(value *InferencePoolImportStatusApplyConfiguration) *InferencePoolImportApplyConfiguration {
+	b.Status = value
+	return b
+}
+
+// GetKind retrieves the value of the Kind field in the declarative configuration.
+func (b *InferencePoolImportApplyConfiguration) GetKind() *string {
+	return b.TypeMetaApplyConfiguration.Kind
+}
+
+// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
+func (b *InferencePoolImportApplyConfiguration) GetAPIVersion() *string {
+	return b.TypeMetaApplyConfiguration.APIVersion
+}
+
+// GetName retrieves the value of the Name field in the declarative configuration.
+func (b *InferencePoolImportApplyConfiguration) GetName() *string {
+	b.ensureObjectMetaApplyConfigurationExists()
+	return b.ObjectMetaApplyConfiguration.Name
+}
+
+// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
+func (b *InferencePoolImportApplyConfiguration) GetNamespace() *string {
+	b.ensureObjectMetaApplyConfigurationExists()
+	return b.ObjectMetaApplyConfiguration.Namespace
+}
diff --git a/client-go/applyconfiguration/apix/v1alpha1/inferencepoolimportstatus.go b/client-go/applyconfiguration/apix/v1alpha1/inferencepoolimportstatus.go
new file mode 100644
index 0000000..9c21414
--- /dev/null
+++ b/client-go/applyconfiguration/apix/v1alpha1/inferencepoolimportstatus.go
@@ -0,0 +1,44 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// InferencePoolImportStatusApplyConfiguration represents a declarative configuration of the InferencePoolImportStatus type for use
+// with apply.
+type InferencePoolImportStatusApplyConfiguration struct {
+	Controllers []ImportControllerApplyConfiguration `json:"controllers,omitempty"`
+}
+
+// InferencePoolImportStatusApplyConfiguration constructs a declarative configuration of the InferencePoolImportStatus type for use with
+// apply.
+func InferencePoolImportStatus() *InferencePoolImportStatusApplyConfiguration {
+	return &InferencePoolImportStatusApplyConfiguration{}
+}
+
+// WithControllers adds the given value to the Controllers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Controllers field.
+func (b *InferencePoolImportStatusApplyConfiguration) WithControllers(values ...*ImportControllerApplyConfiguration) *InferencePoolImportStatusApplyConfiguration {
+	for i := range values {
+		if values[i] == nil {
+			panic("nil value passed to WithControllers")
+		}
+		b.Controllers = append(b.Controllers, *values[i])
+	}
+	return b
+}
diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go
index 8e11c00..7e4ea09 100644
--- a/client-go/applyconfiguration/utils.go
+++ b/client-go/applyconfiguration/utils.go
@@ -23,8 +23,10 @@ import (
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
 	managedfields "k8s.io/apimachinery/pkg/util/managedfields"
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	v1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
 	v1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	apiv1 "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/api/v1"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/apix/v1alpha1"
 	apixv1alpha2 "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/apix/v1alpha2"
 	internal "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/internal"
 )
@@ -51,6 +53,16 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
 	case v1.SchemeGroupVersion.WithKind("Port"):
 		return &apiv1.PortApplyConfiguration{}
 
+		// Group=inference.networking.x-k8s.io, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithKind("ExportingCluster"):
+		return &apixv1alpha1.ExportingClusterApplyConfiguration{}
+	case v1alpha1.SchemeGroupVersion.WithKind("ImportController"):
+		return &apixv1alpha1.ImportControllerApplyConfiguration{}
+	case v1alpha1.SchemeGroupVersion.WithKind("InferencePoolImport"):
+		return &apixv1alpha1.InferencePoolImportApplyConfiguration{}
+	case v1alpha1.SchemeGroupVersion.WithKind("InferencePoolImportStatus"):
+		return &apixv1alpha1.InferencePoolImportStatusApplyConfiguration{}
+
 		// Group=inference.networking.x-k8s.io, Version=v1alpha2
 	case v1alpha2.SchemeGroupVersion.WithKind("Extension"):
 		return &apixv1alpha2.ExtensionApplyConfiguration{}
diff --git a/client-go/clientset/versioned/clientset.go b/client-go/clientset/versioned/clientset.go
index 928ab89..0ea4a00 100644
--- a/client-go/clientset/versioned/clientset.go
+++ b/client-go/clientset/versioned/clientset.go
@@ -26,12 +26,14 @@ import (
 	rest "k8s.io/client-go/rest"
 	flowcontrol "k8s.io/client-go/util/flowcontrol"
 	inferencev1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/api/v1"
+	inferencev1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha1"
 	xinferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha2"
 )
 
 type Interface interface {
 	Discovery() discovery.DiscoveryInterface
 	InferenceV1() inferencev1.InferenceV1Interface
+	InferenceV1alpha1() inferencev1alpha1.InferenceV1alpha1Interface
 	XInferenceV1alpha2() xinferencev1alpha2.XInferenceV1alpha2Interface
 }
 
@@ -39,6 +41,7 @@ type Interface interface {
 type Clientset struct {
 	*discovery.DiscoveryClient
 	inferenceV1        *inferencev1.InferenceV1Client
+	inferenceV1alpha1  *inferencev1alpha1.InferenceV1alpha1Client
 	xInferenceV1alpha2 *xinferencev1alpha2.XInferenceV1alpha2Client
 }
 
@@ -47,6 +50,11 @@ func (c *Clientset) InferenceV1() inferencev1.InferenceV1Interface {
 	return c.inferenceV1
 }
 
+// InferenceV1alpha1 retrieves the InferenceV1alpha1Client
+func (c *Clientset) InferenceV1alpha1() inferencev1alpha1.InferenceV1alpha1Interface {
+	return c.inferenceV1alpha1
+}
+
 // XInferenceV1alpha2 retrieves the XInferenceV1alpha2Client
 func (c *Clientset) XInferenceV1alpha2() xinferencev1alpha2.XInferenceV1alpha2Interface {
 	return c.xInferenceV1alpha2
@@ -100,6 +108,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
 	if err != nil {
 		return nil, err
 	}
+	cs.inferenceV1alpha1, err = inferencev1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	if err != nil {
+		return nil, err
+	}
 	cs.xInferenceV1alpha2, err = xinferencev1alpha2.NewForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
@@ -126,6 +138,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
 func New(c rest.Interface) *Clientset {
 	var cs Clientset
 	cs.inferenceV1 = inferencev1.New(c)
+	cs.inferenceV1alpha1 = inferencev1alpha1.New(c)
 	cs.xInferenceV1alpha2 = xinferencev1alpha2.New(c)
 
 	cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
diff --git a/client-go/clientset/versioned/fake/clientset_generated.go b/client-go/clientset/versioned/fake/clientset_generated.go
index a0709d7..baa42d8 100644
--- a/client-go/clientset/versioned/fake/clientset_generated.go
+++ b/client-go/clientset/versioned/fake/clientset_generated.go
@@ -29,6 +29,8 @@ import (
 	clientset "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned"
 	inferencev1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/api/v1"
 	fakeinferencev1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/api/v1/fake"
+	inferencev1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha1"
+	fakeinferencev1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha1/fake"
 	xinferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha2"
 	fakexinferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha2/fake"
 )
@@ -132,6 +134,11 @@ func (c *Clientset) InferenceV1() inferencev1.InferenceV1Interface {
 	return &fakeinferencev1.FakeInferenceV1{Fake: &c.Fake}
 }
 
+// InferenceV1alpha1 retrieves the InferenceV1alpha1Client
+func (c *Clientset) InferenceV1alpha1() inferencev1alpha1.InferenceV1alpha1Interface {
+	return &fakeinferencev1alpha1.FakeInferenceV1alpha1{Fake: &c.Fake}
+}
+
 // XInferenceV1alpha2 retrieves the XInferenceV1alpha2Client
 func (c *Clientset) XInferenceV1alpha2() xinferencev1alpha2.XInferenceV1alpha2Interface {
 	return &fakexinferencev1alpha2.FakeXInferenceV1alpha2{Fake: &c.Fake}
diff --git a/client-go/clientset/versioned/fake/register.go b/client-go/clientset/versioned/fake/register.go
index 5c6d338..412308d 100644
--- a/client-go/clientset/versioned/fake/register.go
+++ b/client-go/clientset/versioned/fake/register.go
@@ -25,6 +25,7 @@ import (
 	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	inferencev1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
 	xinferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 )
 
@@ -33,6 +34,7 @@ var codecs = serializer.NewCodecFactory(scheme)
 
 var localSchemeBuilder = runtime.SchemeBuilder{
 	inferencev1.AddToScheme,
+	inferencev1alpha1.AddToScheme,
 	xinferencev1alpha2.AddToScheme,
 }
 
diff --git a/client-go/clientset/versioned/scheme/register.go b/client-go/clientset/versioned/scheme/register.go
index 7836df4..47bdc7c 100644
--- a/client-go/clientset/versioned/scheme/register.go
+++ b/client-go/clientset/versioned/scheme/register.go
@@ -25,6 +25,7 @@ import (
 	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	inferencev1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
 	xinferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 )
 
@@ -33,6 +34,7 @@ var Codecs = serializer.NewCodecFactory(Scheme)
 var ParameterCodec = runtime.NewParameterCodec(Scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
 	inferencev1.AddToScheme,
+	inferencev1alpha1.AddToScheme,
 	xinferencev1alpha2.AddToScheme,
 }
 
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/apix_client.go b/client-go/clientset/versioned/typed/apix/v1alpha1/apix_client.go
new file mode 100644
index 0000000..726c410
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/apix_client.go
@@ -0,0 +1,101 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	http "net/http"
+
+	rest "k8s.io/client-go/rest"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+	scheme "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/scheme"
+)
+
+type InferenceV1alpha1Interface interface {
+	RESTClient() rest.Interface
+	InferencePoolImportsGetter
+}
+
+// InferenceV1alpha1Client is used to interact with features provided by the inference.networking.x-k8s.io group.
+type InferenceV1alpha1Client struct {
+	restClient rest.Interface
+}
+
+func (c *InferenceV1alpha1Client) InferencePoolImports(namespace string) InferencePoolImportInterface {
+	return newInferencePoolImports(c, namespace)
+}
+
+// NewForConfig creates a new InferenceV1alpha1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
+func NewForConfig(c *rest.Config) (*InferenceV1alpha1Client, error) {
+	config := *c
+	setConfigDefaults(&config)
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new InferenceV1alpha1Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*InferenceV1alpha1Client, error) {
+	config := *c
+	setConfigDefaults(&config)
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	if err != nil {
+		return nil, err
+	}
+	return &InferenceV1alpha1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new InferenceV1alpha1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *InferenceV1alpha1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new InferenceV1alpha1Client for the given RESTClient.
+func New(c rest.Interface) *InferenceV1alpha1Client {
+	return &InferenceV1alpha1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) {
+	gv := apixv1alpha1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *InferenceV1alpha1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/doc.go b/client-go/clientset/versioned/typed/apix/v1alpha1/doc.go
new file mode 100644
index 0000000..df51baa
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1alpha1
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/fake/doc.go b/client-go/clientset/versioned/typed/apix/v1alpha1/fake/doc.go
new file mode 100644
index 0000000..16f4439
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/fake/fake_apix_client.go b/client-go/clientset/versioned/typed/apix/v1alpha1/fake/fake_apix_client.go
new file mode 100644
index 0000000..1de7688
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/fake/fake_apix_client.go
@@ -0,0 +1,40 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+	v1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha1"
+)
+
+type FakeInferenceV1alpha1 struct {
+	*testing.Fake
+}
+
+func (c *FakeInferenceV1alpha1) InferencePoolImports(namespace string) v1alpha1.InferencePoolImportInterface {
+	return newFakeInferencePoolImports(c, namespace)
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeInferenceV1alpha1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/fake/fake_inferencepoolimport.go b/client-go/clientset/versioned/typed/apix/v1alpha1/fake/fake_inferencepoolimport.go
new file mode 100644
index 0000000..7a6938f
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/fake/fake_inferencepoolimport.go
@@ -0,0 +1,53 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	gentype "k8s.io/client-go/gentype"
+	v1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/apix/v1alpha1"
+	typedapixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/typed/apix/v1alpha1"
+)
+
+// fakeInferencePoolImports implements InferencePoolImportInterface
+type fakeInferencePoolImports struct {
+	*gentype.FakeClientWithListAndApply[*v1alpha1.InferencePoolImport, *v1alpha1.InferencePoolImportList, *apixv1alpha1.InferencePoolImportApplyConfiguration]
+	Fake *FakeInferenceV1alpha1
+}
+
+func newFakeInferencePoolImports(fake *FakeInferenceV1alpha1, namespace string) typedapixv1alpha1.InferencePoolImportInterface {
+	return &fakeInferencePoolImports{
+		gentype.NewFakeClientWithListAndApply[*v1alpha1.InferencePoolImport, *v1alpha1.InferencePoolImportList, *apixv1alpha1.InferencePoolImportApplyConfiguration](
+			fake.Fake,
+			namespace,
+			v1alpha1.SchemeGroupVersion.WithResource("inferencepoolimports"),
+			v1alpha1.SchemeGroupVersion.WithKind("InferencePoolImport"),
+			func() *v1alpha1.InferencePoolImport { return &v1alpha1.InferencePoolImport{} },
+			func() *v1alpha1.InferencePoolImportList { return &v1alpha1.InferencePoolImportList{} },
+			func(dst, src *v1alpha1.InferencePoolImportList) { dst.ListMeta = src.ListMeta },
+			func(list *v1alpha1.InferencePoolImportList) []*v1alpha1.InferencePoolImport {
+				return gentype.ToPointerSlice(list.Items)
+			},
+			func(list *v1alpha1.InferencePoolImportList, items []*v1alpha1.InferencePoolImport) {
+				list.Items = gentype.FromPointerSlice(items)
+			},
+		),
+		fake,
+	}
+}
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/generated_expansion.go b/client-go/clientset/versioned/typed/apix/v1alpha1/generated_expansion.go
new file mode 100644
index 0000000..0969f5b
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+type InferencePoolImportExpansion interface{}
diff --git a/client-go/clientset/versioned/typed/apix/v1alpha1/inferencepoolimport.go b/client-go/clientset/versioned/typed/apix/v1alpha1/inferencepoolimport.go
new file mode 100644
index 0000000..f68766d
--- /dev/null
+++ b/client-go/clientset/versioned/typed/apix/v1alpha1/inferencepoolimport.go
@@ -0,0 +1,74 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	context "context"
+
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	gentype "k8s.io/client-go/gentype"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+	applyconfigurationapixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/applyconfiguration/apix/v1alpha1"
+	scheme "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned/scheme"
+)
+
+// InferencePoolImportsGetter has a method to return a InferencePoolImportInterface.
+// A group's client should implement this interface.
+type InferencePoolImportsGetter interface {
+	InferencePoolImports(namespace string) InferencePoolImportInterface
+}
+
+// InferencePoolImportInterface has methods to work with InferencePoolImport resources.
+type InferencePoolImportInterface interface {
+	Create(ctx context.Context, inferencePoolImport *apixv1alpha1.InferencePoolImport, opts v1.CreateOptions) (*apixv1alpha1.InferencePoolImport, error)
+	Update(ctx context.Context, inferencePoolImport *apixv1alpha1.InferencePoolImport, opts v1.UpdateOptions) (*apixv1alpha1.InferencePoolImport, error)
+	// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+	UpdateStatus(ctx context.Context, inferencePoolImport *apixv1alpha1.InferencePoolImport, opts v1.UpdateOptions) (*apixv1alpha1.InferencePoolImport, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*apixv1alpha1.InferencePoolImport, error)
+	List(ctx context.Context, opts v1.ListOptions) (*apixv1alpha1.InferencePoolImportList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apixv1alpha1.InferencePoolImport, err error)
+	Apply(ctx context.Context, inferencePoolImport *applyconfigurationapixv1alpha1.InferencePoolImportApplyConfiguration, opts v1.ApplyOptions) (result *apixv1alpha1.InferencePoolImport, err error)
+	// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus().
+	ApplyStatus(ctx context.Context, inferencePoolImport *applyconfigurationapixv1alpha1.InferencePoolImportApplyConfiguration, opts v1.ApplyOptions) (result *apixv1alpha1.InferencePoolImport, err error)
+	InferencePoolImportExpansion
+}
+
+// inferencePoolImports implements InferencePoolImportInterface
+type inferencePoolImports struct {
+	*gentype.ClientWithListAndApply[*apixv1alpha1.InferencePoolImport, *apixv1alpha1.InferencePoolImportList, *applyconfigurationapixv1alpha1.InferencePoolImportApplyConfiguration]
+}
+
+// newInferencePoolImports returns a InferencePoolImports
+func newInferencePoolImports(c *InferenceV1alpha1Client, namespace string) *inferencePoolImports {
+	return &inferencePoolImports{
+		gentype.NewClientWithListAndApply[*apixv1alpha1.InferencePoolImport, *apixv1alpha1.InferencePoolImportList, *applyconfigurationapixv1alpha1.InferencePoolImportApplyConfiguration](
+			"inferencepoolimports",
+			c.RESTClient(),
+			scheme.ParameterCodec,
+			namespace,
+			func() *apixv1alpha1.InferencePoolImport { return &apixv1alpha1.InferencePoolImport{} },
+			func() *apixv1alpha1.InferencePoolImportList { return &apixv1alpha1.InferencePoolImportList{} },
+		),
+	}
+}
diff --git a/client-go/informers/externalversions/apix/interface.go b/client-go/informers/externalversions/apix/interface.go
index 26e1817..426cbcd 100644
--- a/client-go/informers/externalversions/apix/interface.go
+++ b/client-go/informers/externalversions/apix/interface.go
@@ -19,12 +19,15 @@ limitations under the License.
 package apix
 
 import (
+	v1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/informers/externalversions/apix/v1alpha1"
 	v1alpha2 "sigs.k8s.io/gateway-api-inference-extension/client-go/informers/externalversions/apix/v1alpha2"
 	internalinterfaces "sigs.k8s.io/gateway-api-inference-extension/client-go/informers/externalversions/internalinterfaces"
 )
 
 // Interface provides access to each of this group's versions.
 type Interface interface {
+	// V1alpha1 provides access to shared informers for resources in V1alpha1.
+	V1alpha1() v1alpha1.Interface
 	// V1alpha2 provides access to shared informers for resources in V1alpha2.
 	V1alpha2() v1alpha2.Interface
 }
@@ -40,6 +43,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
 }
 
+// V1alpha1 returns a new v1alpha1.Interface.
+func (g *group) V1alpha1() v1alpha1.Interface {
+	return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions)
+}
+
 // V1alpha2 returns a new v1alpha2.Interface.
 func (g *group) V1alpha2() v1alpha2.Interface {
 	return v1alpha2.New(g.factory, g.namespace, g.tweakListOptions)
diff --git a/client-go/informers/externalversions/apix/v1alpha1/inferencepoolimport.go b/client-go/informers/externalversions/apix/v1alpha1/inferencepoolimport.go
new file mode 100644
index 0000000..9013e02
--- /dev/null
+++ b/client-go/informers/externalversions/apix/v1alpha1/inferencepoolimport.go
@@ -0,0 +1,102 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	context "context"
+	time "time"
+
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+	gatewayapiinferenceextensionapixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+	versioned "sigs.k8s.io/gateway-api-inference-extension/client-go/clientset/versioned"
+	internalinterfaces "sigs.k8s.io/gateway-api-inference-extension/client-go/informers/externalversions/internalinterfaces"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/client-go/listers/apix/v1alpha1"
+)
+
+// InferencePoolImportInformer provides access to a shared informer and lister for
+// InferencePoolImports.
+type InferencePoolImportInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() apixv1alpha1.InferencePoolImportLister
+}
+
+type inferencePoolImportInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewInferencePoolImportInformer constructs a new informer for InferencePoolImport type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewInferencePoolImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredInferencePoolImportInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredInferencePoolImportInformer constructs a new informer for InferencePoolImport type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredInferencePoolImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.InferenceV1alpha1().InferencePoolImports(namespace).List(context.Background(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.InferenceV1alpha1().InferencePoolImports(namespace).Watch(context.Background(), options)
+			},
+			ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.InferenceV1alpha1().InferencePoolImports(namespace).List(ctx, options)
+			},
+			WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.InferenceV1alpha1().InferencePoolImports(namespace).Watch(ctx, options)
+			},
+		},
+		&gatewayapiinferenceextensionapixv1alpha1.InferencePoolImport{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *inferencePoolImportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredInferencePoolImportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *inferencePoolImportInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&gatewayapiinferenceextensionapixv1alpha1.InferencePoolImport{}, f.defaultInformer)
+}
+
+func (f *inferencePoolImportInformer) Lister() apixv1alpha1.InferencePoolImportLister {
+	return apixv1alpha1.NewInferencePoolImportLister(f.Informer().GetIndexer())
+}
diff --git a/client-go/informers/externalversions/apix/v1alpha1/interface.go b/client-go/informers/externalversions/apix/v1alpha1/interface.go
new file mode 100644
index 0000000..50e84d5
--- /dev/null
+++ b/client-go/informers/externalversions/apix/v1alpha1/interface.go
@@ -0,0 +1,45 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	internalinterfaces "sigs.k8s.io/gateway-api-inference-extension/client-go/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// InferencePoolImports returns a InferencePoolImportInformer.
+	InferencePoolImports() InferencePoolImportInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// InferencePoolImports returns a InferencePoolImportInformer.
+func (v *version) InferencePoolImports() InferencePoolImportInformer {
+	return &inferencePoolImportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/client-go/informers/externalversions/generic.go b/client-go/informers/externalversions/generic.go
index 03c95ee..2fe2915 100644
--- a/client-go/informers/externalversions/generic.go
+++ b/client-go/informers/externalversions/generic.go
@@ -24,6 +24,7 @@ import (
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
 	cache "k8s.io/client-go/tools/cache"
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	v1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
 	v1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 )
 
@@ -57,6 +58,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	case v1.SchemeGroupVersion.WithResource("inferencepools"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Inference().V1().InferencePools().Informer()}, nil
 
+		// Group=inference.networking.x-k8s.io, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithResource("inferencepoolimports"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.XInference().V1alpha1().InferencePoolImports().Informer()}, nil
+
 		// Group=inference.networking.x-k8s.io, Version=v1alpha2
 	case v1alpha2.SchemeGroupVersion.WithResource("inferenceobjectives"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.XInference().V1alpha2().InferenceObjectives().Informer()}, nil
diff --git a/client-go/listers/apix/v1alpha1/expansion_generated.go b/client-go/listers/apix/v1alpha1/expansion_generated.go
new file mode 100644
index 0000000..7c0d32c
--- /dev/null
+++ b/client-go/listers/apix/v1alpha1/expansion_generated.go
@@ -0,0 +1,27 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// InferencePoolImportListerExpansion allows custom methods to be added to
+// InferencePoolImportLister.
+type InferencePoolImportListerExpansion interface{}
+
+// InferencePoolImportNamespaceListerExpansion allows custom methods to be added to
+// InferencePoolImportNamespaceLister.
+type InferencePoolImportNamespaceListerExpansion interface{}
diff --git a/client-go/listers/apix/v1alpha1/inferencepoolimport.go b/client-go/listers/apix/v1alpha1/inferencepoolimport.go
new file mode 100644
index 0000000..653bbfa
--- /dev/null
+++ b/client-go/listers/apix/v1alpha1/inferencepoolimport.go
@@ -0,0 +1,70 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	labels "k8s.io/apimachinery/pkg/labels"
+	listers "k8s.io/client-go/listers"
+	cache "k8s.io/client-go/tools/cache"
+	apixv1alpha1 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1"
+)
+
+// InferencePoolImportLister helps list InferencePoolImports.
+// All objects returned here must be treated as read-only.
+type InferencePoolImportLister interface {
+	// List lists all InferencePoolImports in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*apixv1alpha1.InferencePoolImport, err error)
+	// InferencePoolImports returns an object that can list and get InferencePoolImports.
+	InferencePoolImports(namespace string) InferencePoolImportNamespaceLister
+	InferencePoolImportListerExpansion
+}
+
+// inferencePoolImportLister implements the InferencePoolImportLister interface.
+type inferencePoolImportLister struct {
+	listers.ResourceIndexer[*apixv1alpha1.InferencePoolImport]
+}
+
+// NewInferencePoolImportLister returns a new InferencePoolImportLister.
+func NewInferencePoolImportLister(indexer cache.Indexer) InferencePoolImportLister {
+	return &inferencePoolImportLister{listers.New[*apixv1alpha1.InferencePoolImport](indexer, apixv1alpha1.Resource("inferencepoolimport"))}
+}
+
+// InferencePoolImports returns an object that can list and get InferencePoolImports.
+func (s *inferencePoolImportLister) InferencePoolImports(namespace string) InferencePoolImportNamespaceLister {
+	return inferencePoolImportNamespaceLister{listers.NewNamespaced[*apixv1alpha1.InferencePoolImport](s.ResourceIndexer, namespace)}
+}
+
+// InferencePoolImportNamespaceLister helps list and get InferencePoolImports.
+// All objects returned here must be treated as read-only.
+type InferencePoolImportNamespaceLister interface {
+	// List lists all InferencePoolImports in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*apixv1alpha1.InferencePoolImport, err error)
+	// Get retrieves the InferencePoolImport from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*apixv1alpha1.InferencePoolImport, error)
+	InferencePoolImportNamespaceListerExpansion
+}
+
+// inferencePoolImportNamespaceLister implements the InferencePoolImportNamespaceLister
+// interface.
+type inferencePoolImportNamespaceLister struct {
+	listers.ResourceIndexer[*apixv1alpha1.InferencePoolImport]
+}
diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go
index 2842704..06293c7 100644
--- a/cmd/epp/runner/runner.go
+++ b/cmd/epp/runner/runner.go
@@ -50,6 +50,9 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	dlmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol"
+	fccontroller "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/controller"
+	fcregistry "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/registry"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics/collectors"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
@@ -68,11 +71,25 @@ import (
 )
 
 const (
-	// enableExperimentalDatalayerV2 defines the environment variable
-	// used as feature flag for the pluggable data layer.
+	// enableExperimentalDatalayerV2 defines the environment variable used as feature flag for the pluggable data layer.
 	enableExperimentalDatalayerV2 = "ENABLE_EXPERIMENTAL_DATALAYER_V2"
+	// enableExperimentalFlowControlLayer defines the environment variable used as a feature flag for the pluggable flow
+	// control layer.
+	enableExperimentalFlowControlLayer = "ENABLE_EXPERIMENTAL_FLOW_CONTROL_LAYER"
 )
 
+// TODO: this is hardcoded for POC only. This needs to be hooked up to our text-based config story.
+var flowControlConfig = flowcontrol.Config{
+	Controller: fccontroller.Config{}, // Use all defaults.
+	Registry: fcregistry.Config{
+		// Define domain of accepted priority levels as this field is required. Use defaults for all optional fields.
+		// TODO: this should not be hardcoded.
+		PriorityBands: []fcregistry.PriorityBandConfig{
+			{Priority: 0, PriorityName: "Default"},
+		},
+	},
+}
+
 var (
 	grpcPort       = flag.Int("grpc-port", runserver.DefaultGrpcPort, "The gRPC port used for communicating with Envoy proxy")
 	grpcHealthPort = flag.Int("grpc-health-port", runserver.DefaultGrpcHealthPort, "The port used for gRPC liveness and readiness probes")
@@ -106,6 +123,7 @@ var (
 	modelServerMetricsScheme                  = flag.String("model-server-metrics-scheme", "http", "Scheme to scrape metrics from pods")
 	modelServerMetricsHttpsInsecureSkipVerify = flag.Bool("model-server-metrics-https-insecure-skip-verify", true, "When using 'https' scheme for 'model-server-metrics-scheme', configure 'InsecureSkipVerify' (default to true)")
 	haEnableLeaderElection                    = flag.Bool("ha-enable-leader-election", false, "Enables leader election for high availability. When enabled, readiness probes will only pass on the leader.")
+	tracing                                   = flag.Bool("tracing", true, "Enables emitting traces")
 
 	setupLog = ctrl.Log.WithName("setup")
 )
@@ -141,6 +159,13 @@ func (r *Runner) Run(ctx context.Context) error {
 	flag.Parse()
 	initLogging(&opts)
 
+	if *tracing {
+		err := common.InitTracing(ctx, setupLog)
+		if err != nil {
+			return err
+		}
+	}
+
 	setupLog.Info("GIE build", "commit-sha", version.CommitSHA, "build-ref", version.BuildRef)
 
 	// Validate flags
@@ -244,7 +269,7 @@ func (r *Runner) Run(ctx context.Context) error {
 		runtime.SetBlockProfileRate(1)
 	}
 
-	err = r.parsePluginsConfiguration(ctx)
+	err = r.parsePluginsConfiguration(ctx, datastore)
 	if err != nil {
 		setupLog.Error(err, "Failed to parse plugins configuration")
 		return err
@@ -263,7 +288,43 @@ func (r *Runner) Run(ctx context.Context) error {
 
 	saturationDetector := saturationdetector.NewDetector(sdConfig, setupLog)
 
-	director := requestcontrol.NewDirectorWithConfig(datastore, scheduler, saturationDetector, r.requestControlConfig)
+	// --- Admission Control Initialization ---
+	enableFlowControl := env.GetEnvBool(enableExperimentalFlowControlLayer, false, setupLog)
+	var admissionController requestcontrol.AdmissionController
+	if enableFlowControl {
+		setupLog.Info("Initializing experimental Flow Control layer")
+		fcCfg, err := flowControlConfig.ValidateAndApplyDefaults()
+		if err != nil {
+			setupLog.Error(err, "failed to initialize Flow Control layer")
+			return fmt.Errorf("invalid Flow Control config: %w", err)
+		}
+
+		registry, err := fcregistry.NewFlowRegistry(fcCfg.Registry, setupLog)
+		if err != nil {
+			return fmt.Errorf("failed to initialize Flow Registry: %w", err)
+		}
+		fc, err := fccontroller.NewFlowController(
+			ctx,
+			fcCfg.Controller,
+			registry,
+			saturationDetector,
+			setupLog,
+		)
+		if err != nil {
+			return fmt.Errorf("failed to initialize Flow Controller: %w", err)
+		}
+		go registry.Run(ctx)
+		admissionController = requestcontrol.NewFlowControlAdmissionController(saturationDetector, fc)
+	} else {
+		setupLog.Info("Experimental Flow Control layer is disabled, using legacy admission control")
+		admissionController = requestcontrol.NewLegacyAdmissionController(saturationDetector)
+	}
+
+	director := requestcontrol.NewDirectorWithConfig(
+		datastore,
+		scheduler,
+		admissionController,
+		r.requestControlConfig)
 
 	// --- Setup ExtProc Server Runner ---
 	serverRunner := &runserver.ExtProcServerRunner{
@@ -321,7 +382,7 @@ func (r *Runner) registerInTreePlugins() {
 	plugins.Register(testfilter.HeaderBasedTestingFilterType, testfilter.HeaderBasedTestingFilterFactory)
 }
 
-func (r *Runner) parsePluginsConfiguration(ctx context.Context) error {
+func (r *Runner) parsePluginsConfiguration(ctx context.Context, ds datastore.Datastore) error {
 	if *configText == "" && *configFile == "" {
 		return nil // configuring through code, not through file
 	}
@@ -340,8 +401,9 @@ func (r *Runner) parsePluginsConfiguration(ctx context.Context) error {
 	}
 
 	r.registerInTreePlugins()
-	handle := plugins.NewEppHandle(ctx)
+	handle := plugins.NewEppHandle(ctx, ds.PodList)
 	config, err := loader.LoadConfig(configBytes, handle, logger)
+
 	if err != nil {
 		return fmt.Errorf("failed to load the configuration - %w", err)
 	}
diff --git a/config/charts/inferencepool/README.md b/config/charts/inferencepool/README.md
index 41fee83..f6354bf 100644
--- a/config/charts/inferencepool/README.md
+++ b/config/charts/inferencepool/README.md
@@ -166,30 +166,34 @@ $ helm uninstall pool-1
 
 The following table list the configurable parameters of the chart.
 
-| **Parameter Name**                          | **Description**                                                                                                        |
-|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
-| `inferencePool.apiVersion`                  | The API version of the InferencePool resource. Defaults to `inference.networking.k8s.io/v1`. This can be changed to `inference.networking.x-k8s.io/v1alpha2` to support older API versions. |
-| `inferencePool.targetPortNumber`            | Target port number for the vllm backends, will be used to scrape metrics by the inference extension. Defaults to 8000. |
-| `inferencePool.modelServerType`            | Type of the model servers in the pool, valid options are [vllm, triton-tensorrt-llm], default is vllm. |
-| `inferencePool.modelServers.matchLabels`    | Label selector to match vllm backends managed by the inference pool.                                                   |
-| `inferenceExtension.replicas`               | Number of replicas for the endpoint picker extension service. If More than one replica is used, EPP will run in HA active-passive mode. Defaults to `1`.                                         |
-| `inferenceExtension.image.name`             | Name of the container image used for the endpoint picker.                                                              |
-| `inferenceExtension.image.hub`              | Registry URL where the endpoint picker image is hosted.                                                                |
-| `inferenceExtension.image.tag`              | Image tag of the endpoint picker.                                                                                      |
-| `inferenceExtension.image.pullPolicy`       | Image pull policy for the container. Possible values: `Always`, `IfNotPresent`, or `Never`. Defaults to `Always`.      |
-| `inferenceExtension.env`                    | List of environment variables to set in the endpoint picker container as free-form YAML. Defaults to `[]`.             |
-| `inferenceExtension.extraContainerPorts`    | List of additional container ports to expose. Defaults to `[]`.                                                        |
-| `inferenceExtension.extraServicePorts`      | List of additional service ports to expose. Defaults to `[]`.                                                          |
-| `inferenceExtension.flags`                  | List of flags which are passed through to endpoint picker. Example flags, enable-pprof, grpc-port etc. Refer [runner.go](https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/cmd/epp/runner/runner.go) for complete list.                                                            |
-| `inferenceExtension.affinity`               | Affinity for the endpoint picker. Defaults to `{}`.                                                                    |
-| `inferenceExtension.tolerations`            | Tolerations for the endpoint picker. Defaults to `[]`.                                                                 |   |
-| `inferenceExtension.monitoring.interval`   | Metrics scraping interval for monitoring. Defaults to `10s`.                                                           |
-| `inferenceExtension.monitoring.secret.name` | Name of the service account token secret for metrics authentication. Defaults to `inference-gateway-sa-metrics-reader-secret`. |
-| `inferenceExtension.monitoring.prometheus.enabled` | Enable Prometheus ServiceMonitor creation for EPP metrics collection. Defaults to `false`.                      |
-| `inferenceExtension.monitoring.gke.enabled` | Enable GKE monitoring resources (`PodMonitoring` and RBAC). Defaults to `false`. |
-| `inferenceExtension.pluginsCustomConfig`    | Custom config that is passed to EPP as inline yaml.      |
-| `provider.name`                             | Name of the Inference Gateway implementation being used. Possible values: [`none`, `gke`, or `istio`]. Defaults to `none`.                   |
-| `provider.gke.autopilot` | Set to `true` if the cluster is a GKE Autopilot cluster. This is only used if `provider.name` is `gke`. Defaults to `false`. |
+| **Parameter Name**                                 | **Description**                                                                                                                                                                                                                                                                                                                       |
+|----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `inferencePool.apiVersion`                         | The API version of the InferencePool resource. Defaults to `inference.networking.k8s.io/v1`. This can be changed to `inference.networking.x-k8s.io/v1alpha2` to support older API versions.                                                                                                                                           |
+| `inferencePool.targetPortNumber`                   | Target port number for the vllm backends, will be used to scrape metrics by the inference extension. Defaults to 8000.                                                                                                                                                                                                                |
+| `inferencePool.modelServerType`                    | Type of the model servers in the pool, valid options are [vllm, triton-tensorrt-llm], default is vllm.                                                                                                                                                                                                                                |
+| `inferencePool.modelServers.matchLabels`           | Label selector to match vllm backends managed by the inference pool.                                                                                                                                                                                                                                                                  |
+| `inferenceExtension.replicas`                      | Number of replicas for the endpoint picker extension service. If More than one replica is used, EPP will run in HA active-passive mode. Defaults to `1`.                                                                                                                                                                              |
+| `inferenceExtension.image.name`                    | Name of the container image used for the endpoint picker.                                                                                                                                                                                                                                                                             |
+| `inferenceExtension.image.hub`                     | Registry URL where the endpoint picker image is hosted.                                                                                                                                                                                                                                                                               |
+| `inferenceExtension.image.tag`                     | Image tag of the endpoint picker.                                                                                                                                                                                                                                                                                                     |
+| `inferenceExtension.image.pullPolicy`              | Image pull policy for the container. Possible values: `Always`, `IfNotPresent`, or `Never`. Defaults to `Always`.                                                                                                                                                                                                                     |
+| `inferenceExtension.env`                           | List of environment variables to set in the endpoint picker container as free-form YAML. Defaults to `[]`.                                                                                                                                                                                                                            |
+| `inferenceExtension.extraContainerPorts`           | List of additional container ports to expose. Defaults to `[]`.                                                                                                                                                                                                                                                                       |
+| `inferenceExtension.extraServicePorts`             | List of additional service ports to expose. Defaults to `[]`.                                                                                                                                                                                                                                                                         |
+| `inferenceExtension.flags`                         | List of flags which are passed through to endpoint picker. Example flags, enable-pprof, grpc-port etc. Refer [runner.go](https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/cmd/epp/runner/runner.go) for complete list.                                                                                                                                                                                      |
+| `inferenceExtension.affinity`                      | Affinity for the endpoint picker. Defaults to `{}`.                                                                                                                                                                                                                                                                                   |
+| `inferenceExtension.tolerations`                   | Tolerations for the endpoint picker. Defaults to `[]`.                                                                                                                                                                                                                                                                                |
+| `inferenceExtension.monitoring.interval`           | Metrics scraping interval for monitoring. Defaults to `10s`.                                                                                                                                                                                                                                                                          |
+| `inferenceExtension.monitoring.secret.name`        | Name of the service account token secret for metrics authentication. Defaults to `inference-gateway-sa-metrics-reader-secret`.                                                                                                                                                                                                        |
+| `inferenceExtension.monitoring.prometheus.enabled` | Enable Prometheus ServiceMonitor creation for EPP metrics collection. Defaults to `false`.                                                                                                                                                                                                                                            |
+| `inferenceExtension.monitoring.gke.enabled`        | Enable GKE monitoring resources (`PodMonitoring` and RBAC). Defaults to `false`.                                                                                                                                                                                                                                                      |
+| `inferenceExtension.pluginsCustomConfig`           | Custom config that is passed to EPP as inline yaml.                                                                                                                                                                                                                                                                                   |
+| `inferenceExtension.tracing.enabled`               | Enables or disables OpenTelemetry tracing globally for the EndpointPicker.                                                                                                                                                                                                                                                            |
+| `inferenceExtension.tracing.otelExporterEndpoint`  | OpenTelemetry collector endpoint.                                                                                                                                                                                                                                                                                                     |
+| `inferenceExtension.tracing.sampling.sampler`      | The trace sampler to use. Currently, only `parentbased_traceidratio` is supported. This sampler respects the parent span’s sampling decision when present, and applies the configured ratio for root spans.                                                                                                                           |
+| `inferenceExtension.tracing.sampling.samplerArg`   | Sampler-specific argument. For `parentbased_traceidratio`, this defines the base sampling rate for new traces (root spans), as a float string in the range [0.0, 1.0]. For example, "0.1" enables 10% sampling.                                                                                                                       |
+| `provider.name`                                    | Name of the Inference Gateway implementation being used. Possible values: [`none`, `gke`, or `istio`]. Defaults to `none`.                                                                                                                                                                                                            |
+| `provider.gke.autopilot`                           | Set to `true` if the cluster is a GKE Autopilot cluster. This is only used if `provider.name` is `gke`. Defaults to `false`.                                                                                                                                                                                                          |
 
 ### Provider Specific Configuration
 
@@ -214,6 +218,21 @@ These are the options available to you with `provider.name` set to `istio`:
 | `istio.destinationRule.host`            | Custom host value for the destination rule. If not set this will use the default value which is derrived from the epp service name and release namespace to gerenate a valid service address. |
 | `istio.destinationRule.trafficPolicy.connectionPool`            | Configure the connectionPool level settings of the traffic policy |
 
+#### OpenTelemetry
+
+The EndpointPicker supports OpenTelemetry-based tracing. To enable trace collection, use the following configuration:
+```yaml
+inferenceExtension:
+  tracing:
+    enabled: true
+    otelExporterEndpoint: "http://localhost:4317"
+    sampling:
+      sampler: "parentbased_traceidratio"
+      samplerArg: "0.1"
+```
+Make sure that the `otelExporterEndpoint` points to your OpenTelemetry collector endpoint. 
+Current only the `parentbased_traceidratio` sampler is supported. You can adjust the base sampling ratio using the `samplerArg` (e.g., 0.1 means 10% of traces will be sampled).
+
 ## Notes
 
 This chart will only deploy an InferencePool and its corresponding EndpointPicker extension. Before install the chart, please make sure that the inference extension CRDs are installed in the cluster. For more details, please refer to the [getting started guide](https://gateway-api-inference-extension.sigs.k8s.io/guides/).
diff --git a/config/charts/inferencepool/templates/epp-deployment.yaml b/config/charts/inferencepool/templates/epp-deployment.yaml
index f012c2e..1202402 100644
--- a/config/charts/inferencepool/templates/epp-deployment.yaml
+++ b/config/charts/inferencepool/templates/epp-deployment.yaml
@@ -62,6 +62,12 @@ spec:
         - "--{{ .name }}"
         - "{{ .value }}"
         {{- end }}
+        - "--tracing"
+        {{- if .Values.inferenceExtension.tracing.enabled }}
+        - "true"
+        {{- else }}
+        - "false"
+        {{- end }}
         ports:
         - name: grpc
           containerPort: 9002
@@ -101,6 +107,30 @@ spec:
           valueFrom:
             fieldRef:
               fieldPath: metadata.namespace
+        {{- if .Values.inferenceExtension.tracing.enabled }}
+        - name: OTEL_SERVICE_NAME
+          value: "gateway-api-inference-extension"
+        - name: OTEL_EXPORTER_OTLP_ENDPOINT
+          value: {{ .Values.inferenceExtension.tracing.otelExporterEndpoint | quote }}
+        - name: OTEL_TRACES_EXPORTER
+          value: "otlp"
+        - name: OTEL_RESOURCE_ATTRIBUTES_NODE_NAME
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: spec.nodeName
+        - name: OTEL_RESOURCE_ATTRIBUTES_POD_NAME
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: metadata.name
+        - name: OTEL_RESOURCE_ATTRIBUTES
+          value: 'k8s.namespace.name=$(NAMESPACE),k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)'
+        - name: OTEL_TRACES_SAMPLER
+          value: {{ .Values.inferenceExtension.tracing.sampling.sampler | quote }}
+        - name: OTEL_TRACES_SAMPLER_ARG
+          value: {{ .Values.inferenceExtension.tracing.sampling.samplerArg | quote }}
+        {{- end }}
         {{- if .Values.inferenceExtension.env }}
         {{- toYaml .Values.inferenceExtension.env | nindent 8 }}
         {{- end }}
diff --git a/config/charts/inferencepool/values.yaml b/config/charts/inferencepool/values.yaml
index 91d6a48..f901f7f 100644
--- a/config/charts/inferencepool/values.yaml
+++ b/config/charts/inferencepool/values.yaml
@@ -53,6 +53,12 @@ inferenceExtension:
     
     gke:
       enabled: false
+  tracing:
+    enabled: false
+    otelExporterEndpoint: "http://localhost:4317"
+    sampling:
+      sampler: "parentbased_traceidratio"
+      samplerArg: "0.1"
 
 inferencePool:
   targetPorts:
@@ -85,4 +91,4 @@ istio:
     trafficPolicy: {}
       # connectionPool:
       #   http:
-      #     maxRequestsPerConnection: 256000
+      #     maxRequestsPerConnection: 256000
\ No newline at end of file
diff --git a/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml b/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml
index 2ccaf23..a3f7696 100644
--- a/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml
+++ b/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml
@@ -147,6 +147,8 @@ spec:
                       MatchLabels contains a set of required {key,value} pairs.
                       An object must match every label in this map to be selected.
                       The matching logic is an AND operation on all entries.
+                    maxProperties: 64
+                    minProperties: 1
                     type: object
                 required:
                 - matchLabels
@@ -271,6 +273,25 @@ spec:
                       x-kubernetes-list-map-keys:
                       - type
                       x-kubernetes-list-type: map
+                    controllerName:
+                      description: |-
+                        ControllerName is a domain/path string that indicates the name of the controller that
+                        wrote this status. This corresponds with the GatewayClass controllerName field when the
+                        parentRef references a Gateway kind.
+
+                        Example: "example.net/gateway-controller".
+
+                        The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes names:
+
+                         https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+
+                        Controllers MAY populate this field when writing status. When populating this field, controllers
+                        should ensure that entries to status populated with their ControllerName are cleaned up when they
+                        are no longer necessary.
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+                      type: string
                     parentRef:
                       description: |-
                         ParentRef is used to identify the parent resource that this status
diff --git a/config/crd/bases/inference.networking.x-k8s.io_inferencepoolimports.yaml b/config/crd/bases/inference.networking.x-k8s.io_inferencepoolimports.yaml
new file mode 100644
index 0000000..318e6d4
--- /dev/null
+++ b/config/crd/bases/inference.networking.x-k8s.io_inferencepoolimports.yaml
@@ -0,0 +1,330 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    inference.networking.k8s.io/bundle-version: main-dev
+  name: inferencepoolimports.inference.networking.x-k8s.io
+spec:
+  group: inference.networking.x-k8s.io
+  names:
+    kind: InferencePoolImport
+    listKind: InferencePoolImportList
+    plural: inferencepoolimports
+    shortNames:
+    - infpimp
+    singular: inferencepoolimport
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: InferencePoolImport is the Schema for the InferencePoolImports
+          API.
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          status:
+            description: Status defines the observed state of the InferencePoolImport.
+            properties:
+              controllers:
+                description: Controllers is a list of controllers that are responsible
+                  for managing the InferencePoolImport.
+                items:
+                  description: ImportController defines a controller that is responsible
+                    for managing the InferencePoolImport.
+                  properties:
+                    conditions:
+                      description: |-
+                        Conditions track the state of the InferencePoolImport.
+
+                        Known condition types are:
+
+                         * "Accepted"
+                      items:
+                        description: Condition contains details for one aspect of
+                          the current state of this API Resource.
+                        properties:
+                          lastTransitionTime:
+                            description: |-
+                              lastTransitionTime is the last time the condition transitioned from one status to another.
+                              This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                            format: date-time
+                            type: string
+                          message:
+                            description: |-
+                              message is a human readable message indicating details about the transition.
+                              This may be an empty string.
+                            maxLength: 32768
+                            type: string
+                          observedGeneration:
+                            description: |-
+                              observedGeneration represents the .metadata.generation that the condition was set based upon.
+                              For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                              with respect to the current state of the instance.
+                            format: int64
+                            minimum: 0
+                            type: integer
+                          reason:
+                            description: |-
+                              reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                              Producers of specific condition types may define expected values and meanings for this field,
+                              and whether the values are considered a guaranteed API.
+                              The value should be a CamelCase string.
+                              This field may not be empty.
+                            maxLength: 1024
+                            minLength: 1
+                            pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                            type: string
+                          status:
+                            description: status of the condition, one of True, False,
+                              Unknown.
+                            enum:
+                            - "True"
+                            - "False"
+                            - Unknown
+                            type: string
+                          type:
+                            description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                            maxLength: 316
+                            pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                            type: string
+                        required:
+                        - lastTransitionTime
+                        - message
+                        - reason
+                        - status
+                        - type
+                        type: object
+                      maxItems: 8
+                      type: array
+                      x-kubernetes-list-map-keys:
+                      - type
+                      x-kubernetes-list-type: map
+                    exportingClusters:
+                      description: |-
+                        ExportingClusters is a list of clusters that exported the InferencePool(s) that back the
+                        InferencePoolImport. Required when the controller is responsible for CRUD'ing the InferencePoolImport
+                        from the exported InferencePool(s).
+                      items:
+                        description: ExportingCluster defines a cluster that exported
+                          the InferencePool that backs this InferencePoolImport.
+                        properties:
+                          name:
+                            description: Name of the exporting cluster (must be unique
+                              within the list).
+                            maxLength: 253
+                            minLength: 1
+                            type: string
+                        required:
+                        - name
+                        type: object
+                      type: array
+                    name:
+                      description: |-
+                        Name is a domain/path string that indicates the name of the controller that manages the
+                        InferencePoolImport. Name corresponds to the GatewayClass controllerName field when the
+                        controller will manage parents of type "Gateway". Otherwise, the name is implementation-specific.
+
+                        Example: "example.net/import-controller".
+
+                        The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes
+                        names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+
+                        A controller MUST populate this field when writing status and ensure that entries to status
+                        populated with their controller name are removed when they are no longer necessary.
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+                      type: string
+                    parents:
+                      description: |-
+                        Parents is a list of parent resources, typically Gateways, that are associated with the
+                        InferencePoolImport, and the status of the InferencePoolImport with respect to each parent.
+
+                        Ancestor would be a more accurate name, but Parent is consistent with InferencePool terminology.
+
+                        Required when the controller manages the InferencePoolImport as an HTTPRoute backendRef. The controller
+                        must add an entry for each parent it manages and remove the parent entry when the controller no longer
+                        considers the InferencePoolImport to be associated with that parent.
+                      items:
+                        description: ParentStatus defines the observed state of InferencePool
+                          from a Parent, i.e. Gateway.
+                        properties:
+                          conditions:
+                            description: |-
+                              Conditions is a list of status conditions that provide information about the observed
+                              state of the InferencePool. This field is required to be set by the controller that
+                              manages the InferencePool.
+
+                              Supported condition types are:
+
+                              * "Accepted"
+                              * "ResolvedRefs"
+                            items:
+                              description: Condition contains details for one aspect
+                                of the current state of this API Resource.
+                              properties:
+                                lastTransitionTime:
+                                  description: |-
+                                    lastTransitionTime is the last time the condition transitioned from one status to another.
+                                    This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                                  format: date-time
+                                  type: string
+                                message:
+                                  description: |-
+                                    message is a human readable message indicating details about the transition.
+                                    This may be an empty string.
+                                  maxLength: 32768
+                                  type: string
+                                observedGeneration:
+                                  description: |-
+                                    observedGeneration represents the .metadata.generation that the condition was set based upon.
+                                    For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                                    with respect to the current state of the instance.
+                                  format: int64
+                                  minimum: 0
+                                  type: integer
+                                reason:
+                                  description: |-
+                                    reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                                    Producers of specific condition types may define expected values and meanings for this field,
+                                    and whether the values are considered a guaranteed API.
+                                    The value should be a CamelCase string.
+                                    This field may not be empty.
+                                  maxLength: 1024
+                                  minLength: 1
+                                  pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                                  type: string
+                                status:
+                                  description: status of the condition, one of True,
+                                    False, Unknown.
+                                  enum:
+                                  - "True"
+                                  - "False"
+                                  - Unknown
+                                  type: string
+                                type:
+                                  description: type of condition in CamelCase or in
+                                    foo.example.com/CamelCase.
+                                  maxLength: 316
+                                  pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                                  type: string
+                              required:
+                              - lastTransitionTime
+                              - message
+                              - reason
+                              - status
+                              - type
+                              type: object
+                            maxItems: 8
+                            type: array
+                            x-kubernetes-list-map-keys:
+                            - type
+                            x-kubernetes-list-type: map
+                          controllerName:
+                            description: |-
+                              ControllerName is a domain/path string that indicates the name of the controller that
+                              wrote this status. This corresponds with the GatewayClass controllerName field when the
+                              parentRef references a Gateway kind.
+
+                              Example: "example.net/gateway-controller".
+
+                              The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes names:
+
+                               https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+
+                              Controllers MAY populate this field when writing status. When populating this field, controllers
+                              should ensure that entries to status populated with their ControllerName are cleaned up when they
+                              are no longer necessary.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+                            type: string
+                          parentRef:
+                            description: |-
+                              ParentRef is used to identify the parent resource that this status
+                              is associated with. It is used to match the InferencePool with the parent
+                              resource, such as a Gateway.
+                            properties:
+                              group:
+                                default: gateway.networking.k8s.io
+                                description: |-
+                                  Group is the group of the referent API object. When unspecified, the referent is assumed
+                                  to be in the "gateway.networking.k8s.io" API group.
+                                maxLength: 253
+                                minLength: 0
+                                pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              kind:
+                                default: Gateway
+                                description: |-
+                                  Kind is the kind of the referent API object. When unspecified, the referent is assumed
+                                  to be a "Gateway" kind.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                                type: string
+                              name:
+                                description: Name is the name of the referent API
+                                  object.
+                                maxLength: 253
+                                minLength: 1
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace is the namespace of the referenced object. When unspecified, the local
+                                  namespace is inferred.
+
+                                  Note that when a namespace different than the local namespace is specified,
+                                  a ReferenceGrant object is required in the referent namespace to allow that
+                                  namespace's owner to accept the reference. See the ReferenceGrant
+                                  documentation for details: https://gateway-api.sigs.k8s.io/api-types/referencegrant/
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            required:
+                            - name
+                            type: object
+                        required:
+                        - parentRef
+                        type: object
+                      type: array
+                      x-kubernetes-list-type: atomic
+                  required:
+                  - name
+                  type: object
+                maxItems: 8
+                type: array
+                x-kubernetes-list-map-keys:
+                - name
+                x-kubernetes-list-type: map
+            required:
+            - controllers
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: null
+  storedVersions: null
diff --git a/conformance/resources/base.yaml b/conformance/resources/base.yaml
index a421f50..2e1b378 100644
--- a/conformance/resources/base.yaml
+++ b/conformance/resources/base.yaml
@@ -7,16 +7,16 @@
 apiVersion: v1
 kind: Namespace
 metadata:
-  name: gateway-conformance-infra
+  name: inference-conformance-infra
   labels:
-    gateway-conformance: infra
+    inference-conformance: infra
 ---
 apiVersion: v1
 kind: Namespace
 metadata:
-  name: gateway-conformance-app-backend
+  name: inference-conformance-app-backend
   labels:
-    gateway-conformance: backend
+    inference-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.
@@ -24,7 +24,7 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: Gateway
 metadata:
   name: conformance-primary
-  namespace: gateway-conformance-infra
+  namespace: inference-conformance-infra
 spec:
   gatewayClassName: "{GATEWAY_CLASS_NAME}"
   listeners:
@@ -42,7 +42,7 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: Gateway
 metadata:
   name: conformance-secondary
-  namespace: gateway-conformance-infra
+  namespace: inference-conformance-infra
 spec:
   gatewayClassName: "{GATEWAY_CLASS_NAME}"
   listeners:
@@ -55,14 +55,14 @@ spec:
         from: All
 
 ### The following defines the essential resources for the gateway conformance test.
-### All resources are created in the 'gateway-conformance-app-backend' namespace.
+### All resources are created in the 'inference-conformance-app-backend' namespace.
 ---
 # Deploys a mock backend service to act as a model server.
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: primary-inference-model-server-deployment
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
   labels:
     app: primary-inference-model-server
 spec:
@@ -106,7 +106,7 @@ apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: secondary-inference-model-server-deployment
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
   labels:
     app: secondary-inference-model-server
 spec:
@@ -150,7 +150,7 @@ apiVersion: inference.networking.k8s.io/v1
 kind: InferencePool
 metadata:
   name: primary-inference-pool
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   selector:
     matchLabels:
@@ -167,7 +167,7 @@ apiVersion: v1
 kind: Service
 metadata:
   name: primary-endpoint-picker-svc
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   selector:
     app: primary-app-backend-epp
@@ -183,7 +183,7 @@ apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: primary-app-endpoint-picker
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
   labels:
     app: primary-app-backend-epp
 spec:
@@ -206,7 +206,7 @@ spec:
         - --pool-name
         - "primary-inference-pool"
         - --pool-namespace
-        - "gateway-conformance-app-backend"
+        - "inference-conformance-app-backend"
         - --v
         - "4"
         - --zap-encoder
@@ -247,7 +247,7 @@ apiVersion: inference.networking.k8s.io/v1
 kind: InferencePool
 metadata:
   name: secondary-inference-pool
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   selector:
     matchLabels:
@@ -265,7 +265,7 @@ apiVersion: v1
 kind: Service
 metadata:
   name: secondary-endpoint-picker-svc
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   selector:
     app: secondary-app-backend-epp
@@ -281,7 +281,7 @@ apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: secondary-app-endpoint-picker
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
   labels:
     app: secondary-app-backend-epp
 spec:
@@ -304,7 +304,7 @@ spec:
         - --pool-name
         - "secondary-inference-pool"
         - --pool-namespace
-        - "gateway-conformance-app-backend"
+        - "inference-conformance-app-backend"
         - --v
         - "4"
         - --zap-encoder
@@ -344,7 +344,7 @@ apiVersion: v1
 kind: ConfigMap
 metadata:
   name: plugins-config
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 data:
   conformance-plugins.yaml: |
     apiVersion: inference.networking.x-k8s.io/v1alpha1
@@ -361,7 +361,7 @@ apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
   name: inference-model-reader
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 rules:
 - apiGroups: ["inference.networking.x-k8s.io"]
   resources: ["inferenceobjectives", "inferencepools"]
@@ -377,11 +377,11 @@ apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: epp-to-inference-model-reader
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 subjects:
 - kind: ServiceAccount
   name: default
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 roleRef:
   kind: Role
   name: inference-model-reader
diff --git a/conformance/resources/resourcename.go b/conformance/resources/resourcename.go
index 4c0c7b4..4ac6437 100644
--- a/conformance/resources/resourcename.go
+++ b/conformance/resources/resourcename.go
@@ -19,8 +19,8 @@ package resources
 import "k8s.io/apimachinery/pkg/types"
 
 const (
-	AppBackendNamespace  = "gateway-conformance-app-backend"
-	InfraNamespace       = "gateway-conformance-infra"
+	AppBackendNamespace  = "inference-conformance-app-backend"
+	InfraNamespace       = "inference-conformance-infra"
 	PrimaryGatewayName   = "conformance-primary"
 	SecondaryGatewayName = "conformance-secondary"
 
diff --git a/conformance/scripts/istio/Makefile b/conformance/scripts/istio/Makefile
index 309d8c4..cfce3cc 100644
--- a/conformance/scripts/istio/Makefile
+++ b/conformance/scripts/istio/Makefile
@@ -3,7 +3,7 @@
 GATEWAY_API_VERSION ?= v1.3.0
 INFERENCE_EXTENSION_VERSION ?= v0.4.0
 ISTIO_VERSION ?= 1.27-alpha.0551127f00634403cddd4634567e65a8ecc499a7
-ISTIO_HUB ?= 
+ISTIO_HUB ?=
 ISTIO_PROFILE ?= minimal
 
 # Conformance test variables
@@ -72,7 +72,7 @@ apiVersion: networking.istio.io/v1
 kind: DestinationRule
 metadata:
   name: primary-endpoint-picker-tls
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   host: primary-endpoint-picker-svc
   trafficPolicy:
@@ -84,7 +84,7 @@ apiVersion: networking.istio.io/v1
 kind: DestinationRule
 metadata:
   name: secondary-endpoint-picker-tls
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   host: secondary-endpoint-picker-svc
   trafficPolicy:
@@ -432,7 +432,7 @@ setup-crds: setup-gateway-api-crds setup-inference-extension-crds
 # Setup TLS for EPP
 setup-tls:
 	@echo "Setting up TLS for EPP..."
-	-kubectl create namespace gateway-conformance-app-backend || true
+	-kubectl create namespace inference-conformance-app-backend || true
 	$(file >/tmp/tls-destination-rules.yaml,$(TLS_DESTINATION_RULES))
 	kubectl apply -f /tmp/tls-destination-rules.yaml
 	@rm -f /tmp/tls-destination-rules.yaml
@@ -518,7 +518,7 @@ readme-update: $(REPORT_BASE_DIR)/README.md
 # Clean up resources
 clean:
 	@echo "Cleaning up..."
-	kubectl delete namespace gateway-conformance-app-backend --ignore-not-found=true
+	kubectl delete namespace inference-conformance-app-backend --ignore-not-found=true
 	@echo "Cleaning up downloaded istioctl binaries..."
 	@rm -f $(ISTIOCTL_DIR)/istioctl-*
 	@echo "Note: If using minikube, run 'minikube delete' to completely clean up"
diff --git a/conformance/utils/traffic/traffic.go b/conformance/utils/traffic/traffic.go
index f53cc32..8acaefa 100644
--- a/conformance/utils/traffic/traffic.go
+++ b/conformance/utils/traffic/traffic.go
@@ -169,24 +169,56 @@ func waitForConvergeToExpected(
 	tlog.Logf(t, "Request passed")
 }
 
-// CompareRequestWithWildcardStatus compares requests with wildcard status code support.
-// It treats a single-digit expected code (e.g., 4) as a class wildcard (4xx),
-// while standard 3-digit codes are matched exactly.
-func CompareRequestWithWildcardStatus(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected gwhttp.ExpectedResponse) error {
-	if expected.Response.StatusCode < 1 || expected.Response.StatusCode >= 100 {
-		return gwhttp.CompareRequest(t, req, cReq, cRes, expected)
+// CompareRequestWithWildcardStatus compares requests allowing single-digit
+// status "classes" (e.g. 5 => any 5xx) via ExpectedResponse.Response.StatusCodes.
+// If no class wildcards are present, it defers to CompareRoundTrip.
+func CompareRequestWithWildcardStatus(
+	t *testing.T,
+	req *roundtripper.Request,
+	cReq *roundtripper.CapturedRequest,
+	cRes *roundtripper.CapturedResponse,
+	expected gwhttp.ExpectedResponse,
+) error {
+	// Separate specific status codes (>=100) from wildcard classes (1..9).
+	var wildcardClasses []int
+	var specificCodes []int
+	seenClass := map[int]struct{}{}
+	seenCode := map[int]struct{}{}
+
+	for _, sc := range expected.Response.StatusCodes {
+		switch {
+		case sc >= 100:
+			if _, ok := seenCode[sc]; !ok {
+				specificCodes = append(specificCodes, sc)
+				seenCode[sc] = struct{}{}
+			}
+		case sc >= 1 && sc <= 9:
+			if _, ok := seenClass[sc]; !ok {
+				wildcardClasses = append(wildcardClasses, sc)
+				seenClass[sc] = struct{}{}
+			}
+		}
 	}
 
-	expectedClass := expected.Response.StatusCode
+	// No wildcard classes? Defer to standard comparator.
+	if len(wildcardClasses) == 0 {
+		return gwhttp.CompareRoundTrip(t, req, cReq, cRes, expected)
+	}
+
+	// If the concrete status matches any wildcard class, materialize it and compare.
 	actualClass := cRes.StatusCode / 100
-	if expectedClass != actualClass {
-		return fmt.Errorf("expected status code class %dxx, but got %d", expectedClass, cRes.StatusCode)
+	for _, wc := range wildcardClasses {
+		if actualClass == wc {
+			modified := expected
+			// Keep any specific codes that were provided, but ensure the actual status is allowed.
+			modified.Response.StatusCodes = append(append([]int(nil), specificCodes...), cRes.StatusCode)
+			return gwhttp.CompareRoundTrip(t, req, cReq, cRes, modified)
+		}
 	}
 
-	// StatusCode Class matches; update status code on a copy to allow the standard comparator to pass.
-	modifiedExpected := expected
-	modifiedExpected.Response.StatusCode = cRes.StatusCode
-	return gwhttp.CompareRequest(t, req, cReq, cRes, modifiedExpected)
+	// Wildcards were provided but none matched the actual class—fail clearly.
+	return fmt.Errorf("expected status code class to be one of %vxx, got %d",
+		wildcardClasses, cRes.StatusCode)
 }
 
 // TODO: https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1031
diff --git a/crd-ref-docs.yaml b/crd-ref-docs.yaml
index d00e6d0..5fefbff 100644
--- a/crd-ref-docs.yaml
+++ b/crd-ref-docs.yaml
@@ -4,7 +4,7 @@
 
 processor:
   ignoreTypes:
-    - "(InferencePool|InferenceObjective)List$"
+    - "(InferencePool|InferenceObjective|InferencePoolImport)List$"
   # RE2 regular expressions describing type fields that should be excluded from the generated documentation.
   ignoreFields:
     - "TypeMeta$"
diff --git a/docs/proposals/004-endpoint-picker-protocol/README.md b/docs/proposals/004-endpoint-picker-protocol/README.md
index 03b96bb..919bb63 100644
--- a/docs/proposals/004-endpoint-picker-protocol/README.md
+++ b/docs/proposals/004-endpoint-picker-protocol/README.md
@@ -14,6 +14,15 @@ This doc defines the protocol between the EPP and the proxy (e.g, Envoy).
 The EPP MUST implement the Envoy
 [external processing service](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/ext_proc/v3/ext_proc.proto) protocol.
 
+## Version History
+
+| Version  | Date       | Changes                                          |
+|----------|------------|--------------------------------------------------|
+| v1.0.0   | 2025-07-29 | Added status metadata field for picked endpoints |
+| v0.4.0   | 2025-06-03 | Added support for multiple fallback endpoints    |
+| v0.3.0   | 2025-03-14 | Added subsetting and fallback support            |
+| v0.2.0   | 2025-02-22 | Initial protocol definition                      |
+
 ## Endpoint Subset
 
 [REQUEST: Data Plane -> EPP]
diff --git a/docs/proposals/1374-multi-cluster-inference/README.md b/docs/proposals/1374-multi-cluster-inference/README.md
new file mode 100644
index 0000000..321b0d2
--- /dev/null
+++ b/docs/proposals/1374-multi-cluster-inference/README.md
@@ -0,0 +1,493 @@
+# Multi-Cluster InferencePools
+
+Author(s): @danehans, @bexxmodd, @robscott
+
+## Proposal Status
+
+ ***Draft***
+
+## Summary
+
+An Inference Gateway (IG) provides efficient routing to LLM workloads in Kubernetes by sending requests to an Endpoint Picker (EPP) associated with
+an [InferencePool](https://gateway-api-inference-extension.sigs.k8s.io/api-types/inferencepool/) and routing the request to a backend model server
+based on the EPP-provided endpoint. Although other multi-cluster inference approaches may exist, this proposal extends the current model to support
+multi-cluster routing so capacity in one cluster can serve traffic originating in another cluster or outside the clusters.
+
+### Why Multi-Cluster?
+
+GPU capacity is scarce and fragmented. Many users operate multiple clusters across regions and providers. A single cluster rarely satisfies peak or
+sustained demand, so a prescribed approach is required to share GPU capacity across clusters by:
+
+- Exporting an InferencePool from a source (“exporting”) cluster.
+- Importing the exported InferencePool into one or more destination (“importing”) clusters with enough detail for IGs to route requests to the associated
+  remote model server Pods.
+
+### Goals
+
+- Enable IGs to route to a group of common model server Pods, e.g. InferencePools, that exist in different clusters.
+- Align the UX with familiar [Multi-Cluster Services (MCS)](https://multicluster.sigs.k8s.io/concepts/multicluster-services-api/) concepts (export/import).
+- Keep the API simple and implementation-agnostic.
+
+### Non-Goals
+
+- Managing DNS or automatic naming.
+- Over-specifying implementation details to satisfy a single approach to Multi-Cluster InferencePools.
+
+## Design Proposal
+
+The Multi-Cluster InferencePools (MCIP) model will largely follow the Multi-Cluster Services (MCS) model, with a few key differences:
+
+- DNS and ClusterIP resolution will be omitted, e.g. ClusterSetIP.
+- A separate export resource will be avoided, e.g. ServiceExport, by inlining the concept within InferencePool.
+
+An InferencePoolImport resource is introduced that is meant to be fully managed by a controller. This resource provides the information
+required for IGs to route LLM requests to model server endpoints of an InferencePool in remote clusters. How the IG routes the request to the remote
+cluster is implementation-specific.
+
+### Routing Modes
+
+An implementation must support at least one of the following routing modes:
+
+- Endpoint Mode: An IG of an importing cluster routes to endpoints selected by the EPP of the exported InferencePool. Pod and Service network connectivity
+  MUST exist between cluster members.
+- Parent Mode: An IG of an importing cluster routes to parents, e.g. Gateways, of the exported InferencePool. Parent connectivity MUST exist between cluster
+  members.
+
+### Sync Topology (Implementation-Specific)
+
+An implementation must support at least one of the following distribution topologies. The API does not change between them (same export annotation and InferencePoolImport).
+
+1. **Hub/Spoke**
+   - A hub controller has visibility into member clusters.
+   - It watches exported InferencePools and creates/updates the corresponding InferencePoolImport (same namespace/name) in each member cluster.
+   - Typical when a central control plane has K8s API server access for each member cluster.
+   - Consider [KEP-5339-style](https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/5339-clusterprofile-plugin-credentials) pluggable credential issuance to avoid hub-stored long-lived secrets.
+
+2. **Push/Pull**
+   - A cluster-local controller watches exported InferencePools and publishes export state to a central hub.
+   - A cluster-local controller watches the central hub and CRUDs the local InferencePoolImport.
+   - Typical when you want no hub-stored member credentials, looser coupling, and fleet-scale fan-out.
+
+### Workflow
+
+1. **Export an InferencePool:** An [Inference Platform Owner](https://gateway-api-inference-extension.sigs.k8s.io/concepts/roles-and-personas/)
+   exports an InferencePool by annotating it.
+2. **Distribution (topology-dependent, API-agnostic):**
+   - **Hub/Spoke:** A central hub controller watches exported InferencePools and mirrors a same-name/namespace InferencePoolImport into each member cluster, updating `status.controllers[]` to reflect the managing controller, exporting clusters, etc..
+   - **Push/Pull:** A cluster-local controller watches exported InferencePools and publishes export records to a central hub. In each member cluster, a controller watches the hub and CRUDs the local InferencePoolImport (same name/namespace) and maintains `status.controllers[]`.
+3. **Importing Controller (common):**
+   - Watches local InferencePoolImport and:
+     - Programs the IG dataplane based on the supported routing mode.
+     - If this controller differs from the Exporting Controller, populate `status.controllers[]`.
+     - Manages `status.controllers[].parentRefs` with the Group, Kind, and Name of the local parent resource, e.g. Gateway, of the InferencePoolImport.
+4. **Data Path:**
+   The data path is dependent on the export mode selected by the implementation.
+   - Endpoint Mode: Client → local IG → (make scheduling decision) → local/remote EPP → selected model server endpoint → response.
+   - Parent Mode: Client → local IG → (make scheduling decision) → local EPP/remote parent → remote EPP → selected model server endpoint → response.
+
+### InferencePoolImport Naming
+
+The exporting controller will create an InferencePoolImport resource using the exported InferencePool namespace and name. A cluster name entry in
+`status.controllers[]` is added for each cluster that exports an InferencePool with the same ns/name.
+
+**Note:** EPP ns/name sameness is not required.
+
+### InferencePool Selection
+
+InferencePool selection is implementation-specific. The following are examples of how an IG may select one exported InferencePool over another:
+
+- **Metrics-based:** Scrape EPP-exposed metrics (e.g., ready pods) to bias InferencePool choice.
+- **Active-Passive:** Basic EPP readiness checks (gRPC health).
+
+**Note:** When an exported InferencePool is selected by an IG, standard EPP semantics are used to select endpoints of that pool.
+
+### API Changes
+
+#### InferencePool Annotation
+
+The following annotation is being proposed to indicate the desire to export the InferencePool to member clusters of a ClusterSet.
+
+The `inference.networking.x-k8s.io/export` annotation key indicates a desire to export the InferencePool:
+
+```yaml
+inference.networking.x-k8s.io/export: "<value>"
+```
+
+Supported Values:
+
+- `ClusterSet` – export to all members of the current [ClusterSet](https://multicluster.sigs.k8s.io/api-types/cluster-set/).
+
+**Note:** Additional annotations, e.g. region/domain scoping, filter clusters in the ClusterSet, routing mode configuration, etc. and
+potentially adding an InferencePoolExport resource may be considered in the future.
+
+#### InferencePool Status
+
+An implementation MUST set a parent status entry with a parentRef of kind `InferencePoolImport` and the ns/name of the exported InferencePool.
+This informs the user that the request to export the InferencePool has been recognized by the implementation, along with the implementation's
+unique `ControllerName`.  `ControllerName` is a domain/path string that indicates the name of the controller that wrote the status entry.
+
+An `Exported` parent condition type is being added to surface status of the exported InferencePool. An implementation MUST set
+this status condition to `True` when the annotated InferencePool has been exported to all member clusters of the ClusterSet and `False`
+for all other reasons. When the export annotation is removed from the InferencePool, an implementation MUST remove this condition type.
+
+#### InferencePoolImport
+
+A cluster-local, controller-managed resource that represents an imported InferencePool. It primarily communicates a relationship between an exported
+InferencePool and the exporting cluster name. It is not user-authored; status carries the effective import. Inference Platform Owners can reference
+the InferencePoolImport, even if the local cluster does not have an InferencePool. In the context of Gateway API, it means that an HTTPRoute can be
+configured to reference an InferencePoolImport to route matching requests to remote InferencePool endpoints. This API will be used almost exclusively
+for tracking endpoints, but unlike MCS, we actually have two distinct sets of endpoints to track:
+
+1. Endpoint Picker Extensions (EPPs)
+2. InferencePool parents, e.g. Gateways
+
+Key ideas:
+
+- Map exported InferencePool to exporting controller and cluster.
+- Name/namespace sameness with the exported InferencePool (avoids extra indirection).
+- Conditions: Surface a controller-level status condition to indicate that the InferencePoolImport is ready to be used.
+- Conditions: Surface parent-level status conditions to indicate that the InferencePoolImport is referenced by a parent, e.g. Gateway.
+
+See the full Go type below for additional details.
+
+## Controller Responsibilities
+
+**Export Controller:**
+
+- Discover exported InferencePools.
+- For each ClusterSet member cluster, CRUD InferencePoolImport (mirrored namespace/name).
+- Populate the exported InferencePool with status to indicate a unique name of the managing controller and an `Exported`  condition that
+  indicates the status of the exported pool.
+- Populate an InferencePoolImport `status.controllers[]` entry with the managing controller name, cluster, and status conditions associated
+  with the exported InferencePool.
+
+**Import Controller:**
+
+- Watch InferencePoolImports.
+- Program the IG data plane to route matching requests based on the supported routing mode.
+- Manage InferencePoolImport `status.controllers[].parentRefs` with a cluster-local parentRef, e.g. Gateway, when the InferencePoolImport
+  is referenced by a managed HTTProute.
+
+## Examples
+
+### Exporting Cluster (Cluster A) Manifests
+
+In this example, Cluster A exports the InferencePool to all clusters in the ClusterSet. This will
+cause the exporting controller to create an InferencePoolImport resource in all clusters.
+
+```yaml
+apiVersion: inference.networking.k8s.io/v1
+kind: InferencePool
+metadata:
+  name: llm-pool
+  namespace: example
+  annotations:
+    inference.networking.x-k8s.io/export: "ClusterSet" # Export the pool to all clusters in the ClusterSet
+spec:
+  endpointPickerRef:
+    name: epp
+    portNumber: 9002
+  selector:
+    matchLabels:
+      app: my-model
+  targetPorts:
+  - number: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: epp
+  namespace: example
+spec:
+  selector:
+    app: epp
+  ports:
+  - name: ext-proc
+    port: 9002
+    targetPort: 9002
+    appProtocol: http2
+  type: LoadBalancer # EPP exposed via LoadBalancer
+```
+
+### Importing Cluster (Cluster B) Manifests
+
+In this example, the Inference Platform Owner has configured an HTTPRoute to route to endpoints of the Cluster A InferencePool
+by referencing the InferencePoolImport as a `backendRef`. The parent IG(s) of the HTTPRoute are responsible for routing to the
+endpoints selected by the exported InferencePool's EPP.
+
+The InferencePoolImport is controller-managed; shown here only to illustrate the expected status shape.
+
+```yaml
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: InferencePoolImport
+metadata:
+  name: llm-pool      # mirrors exporting InferencePool name
+  namespace: example  # mirrors exporting InferencePool namespace
+status:
+  controllers:
+  - controllerName: example.com/mcip-controller
+    type: MultiCluster
+    exportingClusters:
+    - name: cluster-a
+  - controllerName: example.com/ig-controller
+    type: GatewayClass
+    parents:
+    - parentRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: inf-gw # Cluster-local parent, e.g. gateway
+      conditions:
+      - type: Accepted
+        status: "True"
+---
+# Route in the importing cluster that targets the imported pool
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: HTTPRoute
+metadata:
+  name: llm-route
+  namespace: example
+spec:
+  parentRefs:
+  - name: inf-gw
+  hostnames:
+  - my.model.com
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /completions
+    backendRefs:
+    - group: inference.networking.x-k8s.io
+      kind: InferencePoolImport
+      name: llm-pool
+```
+
+An implementation MUST conform to Gateway API specifications, including when the HTTPRoute contains InferencePool and InferencePoolImport `backendRefs`,
+e.g. `weight`-based load balancing. In the following example, traffic MUST be split equally between Cluster A and B InferencePool endpoints when
+using the following `backendRefs`:
+
+```yaml
+    backendRefs:
+    - group: inference.networking.k8s.io
+      kind: InferencePool
+      name: llm-pool
+      weight: 50
+    - group: inference.networking.x-k8s.io
+      kind: InferencePoolImport
+      name: llm-pool
+      weight: 50
+```
+
+**Note:** The above example does not export the local "llm-pool" InferencePool. If this InferencePool was exported, it would be included in
+the example InferencePoolImport and the implementation would be responsible for balancing the traffic between the two pools.
+
+### Go Types
+
+The following Go types define the InferencePoolImport API being introduced by this proposal. Note that a separate Pull Request will be used
+to finalize and merge all Go types into the repository.
+
+```go
+package v1alpha1
+
+import (
+    corev1 "k8s.io/api/core/v1"
+    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+    v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:scope=Namespaced,shortName=ipimp
+// +kubebuilder:subresource:status
+//
+// InferencePoolImport represents an imported InferencePool from another cluster.
+// This resource is controller-managed; users typically do not author it directly.
+type InferencePoolImport struct {
+    metav1.TypeMeta   `json:",inline"`
+    metav1.ObjectMeta `json:"metadata,omitempty"`
+
+    // Spec defines the desired state of the InferencePoolImport.
+    Spec InferencePoolImportSpec `json:"spec,omitempty"`
+
+    // Status defines the current state of the InferencePoolImport.
+    Status InferencePoolImportStatus `json:"status,omitempty"`
+}
+
+// Unused but defined for potential future use.
+type InferencePoolImportSpec struct{}
+
+type InferencePoolImportStatus struct {
+    // Controllers is a list of controllers that are responsible for managing this InferencePoolImport.
+    //
+    // +kubebuilder:validation:Required
+    Controllers []ImportController `json:"controllers"`
+}
+
+// ImportController defines a controller that is responsible for managing this InferencePoolImport.
+type ImportController struct {
+    // Name is a domain/path string that indicates the name of the controller that manages this
+    // InferencePoolImport. This corresponds to the GatewayClass controllerName field when the
+    // controller will manage parents of type "Gateway". Otherwise, the name is implementation-specific.
+    //
+    // Example: "example.net/import-controller".
+    //
+    // The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes
+    // names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+    //
+    // A controller MUST populate this field when writing status and ensure that entries to status
+    // populated with their controller name are removed when they are no longer necessary.
+    //
+    // +required
+    Name ControllerName `json:"name"`
+
+    // ExportingClusters is a list of clusters that exported the InferencePool associated with this
+    // InferencePoolImport. Required when the controller is responsible for CRUD'ing the InferencePoolImport
+    // from the exported InferencePool(s).
+    //
+    // +optional
+    ExportingClusters []ExportingCluster `json:"exportingClusters"`
+
+    // Parents is a list of parent resources, typically Gateways, that are associated with the
+    // InferencePoolImport, and the status of the InferencePoolImport with respect to each parent.
+    //
+    // Required when the controller manages the InferencePoolImport as an HTTPRoute backendRef. The controller
+    // must add an entry for each parent it manages and remove the parent entry when the controller no longer
+    // considers the InferencePoolImport to be associated with that parent.
+    //
+    // +optional
+    // +listType=atomic
+    Parents []v1.ParentStatus `json:"parents,omitempty"`
+
+    // Conditions track the state of the InferencePoolImport.
+    //
+    // Known condition types are:
+    //
+    //  * "Accepted"
+    //
+    // +optional
+    // +listType=map
+    // +listMapKey=type
+    // +kubebuilder:validation:MaxItems=8
+    Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// ControllerName is the name of a controller that manages a resource. It must be a domain prefixed path.
+//
+// Valid values include:
+//
+//  * "example.com/bar"
+//
+// Invalid values include:
+//
+//  * "example.com" - must include path
+//  * "foo.example.com" - must include path
+//
+// +kubebuilder:validation:MinLength=1
+// +kubebuilder:validation:MaxLength=253
+// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$`
+type ControllerName string
+
+// ExportingCluster defines a cluster that exported the InferencePool associated to this InferencePoolImport.
+type ExportingCluster struct {
+    // Name of the exporting cluster (must be unique within the list). 
+    //
+    // +kubebuilder:validation:Required
+    Name string `json:"name"`
+}
+
+// +kubebuilder:object:root=true
+type InferencePoolImportList struct {
+    metav1.TypeMeta `json:",inline"`
+    metav1.ListMeta `json:"metadata,omitempty"`
+    Items           []InferencePoolImport `json:"items"`
+}
+```
+
+### Failure Mode
+
+EPP failure modes continue to work as-is and are independent of MCIP.
+
+#### EPP Selection
+
+Since an IG decides which EPP to use for endpoint selection when multiple InferencePool/InferencePoolImport `backendRefs` exist,
+an implementation MAY use EPP metrics and/or health data to make a load-balancing decision.
+
+## Alternatives
+
+### Option 1: Reuse MCS API for EPP
+
+Reuse MCS to export EPP Services. This approach provides simple infra, but may be confusing to users (you “export EPPs” not pools) and
+requires a separate MCS parent export for parent-based inter-cluster routing.
+
+**Pros**:
+
+- Reuses existing MCS infrastructure.
+- Relatively simple to implement.
+
+**Cons**:
+
+- Referencing InferencePools in other clusters requires you to create an InferencePool locally.
+- In this model, you don’t actually choose to export an InferencePool, you export the EPP or InferencePool parent(s) service, that could lead to confusion.
+- InferencePool is meant to be a replacement for a Service so it may seem counterintuitive for a user to create a Service to achieve multi-cluster inference.
+
+## Option 2: New MCS API
+
+One of the key pain points we’re seeing here is that the current iteration of the MCS API requires a tight coupling between name/namespace and kind, with Service being the only kind of backend supported right now. This goes against the broader SIG-Network direction of introducing more focused kinds of backends (like InferencePool). To address this, we could create a resource that has an `exportRef` that allows for exporting different types of resources.
+
+While we were at it, we could combine the separate `export` and `import` resources that exist today, with `export` acting as the (optional) spec of this new resource, and `import` acting as `status` of the resource. Instead of `import` resources being automatically created, users would create them wherever they wanted to reference or export something to a MultiClusterService.
+
+Here’s a very rough example:
+
+```yaml
+apiVersion: networking.k8s.io/v1
+kind: MultiClusterService
+metadata:
+  name: epp
+  namespace: example
+spec:
+  exportRef:
+    group: v1
+    kind: Service
+    name: epp
+    scope: ClusterSet
+status:
+  conditions:
+  - type: Accepted
+    status: "True"
+    message: "MultiClusterService has been accepted"
+    lastTransitionTime: "2025-03-30T01:33:51Z"
+  targetCount: 1
+  ports:
+  - protocol: TCP
+    appProtocol: HTTP
+    port: 8080
+```
+
+### Open Questions
+
+#### EPP Discovery
+
+- Should EPP Deployment/Pod discovery be standardized (labels/port names) for health/metrics auto-discovery?
+
+#### Security
+
+- Provide a standard way to bootstrap mTLS between importing IG and exported EPP/parents, e.g. use BackendTLSPolicy?
+
+#### Ownership and Lifecycle
+
+- Garbage collection when export is withdrawn (delete import?) and how to drain traffic safely. See [this comment](https://github.com/kubernetes-sigs/gateway-api-inference-extension/pull/1374#discussion_r2357523781) for additional context.
+
+### Prior Art
+
+- [GEP-1748: Gateway API Interaction with Multi-Cluster Services](https://gateway-api.sigs.k8s.io/geps/gep-1748/)
+- [Envoy Gateway with Multi-Cluster Services](https://gateway.envoyproxy.io/latest/tasks/traffic/multicluster-service/)
+- [Multi-Cluster Service API](https://multicluster.sigs.k8s.io/concepts/multicluster-services-api/)
+
+### References
+
+- [Initial Multi-Cluster Inference Design Doc](https://docs.google.com/document/d/1QGvG9ToaJ72vlCBdJe--hmrmLtgOV_ptJi9D58QMD2w/edit?tab=t.0#heading=h.q6xiq2fzcaia)
+
+### Notes for reviewers
+
+- The InferencePoolImport CRD is intentionally status-only to keep the UX simple and controller-driven.
+- The InferencePool namespace sameness simplifies identity and lets HTTPRoute authors reference imports without new indirection.
diff --git a/go.mod b/go.mod
index 2062225..982d46b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,33 +5,38 @@ go 1.24.0
 require (
 	github.com/cespare/xxhash/v2 v2.3.0
 	github.com/elastic/crd-ref-docs v0.2.0
-	github.com/envoyproxy/go-control-plane/envoy v1.32.4
+	github.com/envoyproxy/go-control-plane/envoy v1.35.0
 	github.com/go-logr/logr v1.4.3
 	github.com/google/go-cmp v0.7.0
 	github.com/google/uuid v1.6.0
 	github.com/hashicorp/golang-lru/v2 v2.0.7
-	github.com/onsi/ginkgo/v2 v2.25.3
+	github.com/onsi/ginkgo/v2 v2.26.0
 	github.com/onsi/gomega v1.38.2
 	github.com/prometheus/client_golang v1.23.2
 	github.com/prometheus/client_model v0.6.2
-	github.com/prometheus/common v0.66.1
+	github.com/prometheus/common v0.67.1
 	github.com/prometheus/prometheus v0.306.0
 	github.com/stretchr/testify v1.11.1
+	go.opentelemetry.io/otel v1.38.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0
+	go.opentelemetry.io/otel/sdk v1.38.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/sync v0.17.0
-	google.golang.org/grpc v1.75.1
-	google.golang.org/protobuf v1.36.9
+	google.golang.org/grpc v1.76.0
+	google.golang.org/protobuf v1.36.10
 	k8s.io/api v0.34.1
 	k8s.io/apiextensions-apiserver v0.34.1
 	k8s.io/apimachinery v0.34.1
 	k8s.io/client-go v0.34.1
 	k8s.io/code-generator v0.34.1
 	k8s.io/component-base v0.34.1
-	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
-	sigs.k8s.io/controller-runtime v0.21.0
+	k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
+	sigs.k8s.io/controller-runtime v0.22.3
+	// Update the CONTROLLER_TOOLS_VERSION in Makefile when bumping controller-tools.
 	sigs.k8s.io/controller-tools v0.19.0
-	sigs.k8s.io/gateway-api v1.3.0
+	sigs.k8s.io/gateway-api v1.4.0
 	sigs.k8s.io/structured-merge-diff/v6 v6.3.0
 	sigs.k8s.io/yaml v1.6.0
 )
@@ -45,11 +50,11 @@ require (
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/blang/semver/v4 v4.0.0 // indirect
-	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
 	github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dennwc/varint v1.0.0 // indirect
-	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -57,9 +62,9 @@ require (
 	github.com/fxamacker/cbor/v2 v2.9.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/jsonpointer v0.21.2 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
-	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-openapi/swag v0.23.1 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gobuffalo/flect v1.0.3 // indirect
 	github.com/goccy/go-yaml v1.18.0 // indirect
@@ -70,14 +75,14 @@ require (
 	github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // indirect
 	github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
 	github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
 	github.com/huandu/xstrings v1.3.3 // 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
 	github.com/kylelemons/godebug v1.1.0 // indirect
-	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mailru/easyjson v0.9.0 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/spdystream v0.5.0 // indirect
@@ -85,50 +90,45 @@ require (
 	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/prometheus/procfs v0.17.0 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
 	github.com/spf13/pflag v1.0.7 // indirect
 	github.com/stoewer/go-strcase v1.3.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
-	go.opentelemetry.io/otel v1.37.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
-	go.opentelemetry.io/otel/metric v1.37.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
-	go.opentelemetry.io/otel/trace v1.37.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
+	go.opentelemetry.io/otel/metric v1.38.0 // indirect
+	go.opentelemetry.io/otel/trace v1.38.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.1 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
-	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/crypto v0.42.0 // indirect
 	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/mod v0.28.0 // indirect
 	golang.org/x/net v0.44.0 // indirect
-	golang.org/x/oauth2 v0.30.0 // indirect
+	golang.org/x/oauth2 v0.31.0 // indirect
 	golang.org/x/sys v0.36.0 // indirect
 	golang.org/x/term v0.35.0 // indirect
 	golang.org/x/text v0.29.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
 	golang.org/x/tools v0.37.0 // indirect
-	golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
-	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
 	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.34.1 // indirect
-	k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect
+	k8s.io/gengo/v2 v2.0.0-20250820003526-c297c0c1eb9d // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
-	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
+	k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
-	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
 	sigs.k8s.io/randfill v1.0.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 6586a87..1805d42 100644
--- a/go.sum
+++ b/go.sum
@@ -60,8 +60,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
-github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
-github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
@@ -75,10 +75,10 @@ github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
 github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
 github.com/elastic/crd-ref-docs v0.2.0 h1:U17MyGX71j4qfKTvYxbR4qZGoA1hc2thy7kseGYmP+o=
 github.com/elastic/crd-ref-docs v0.2.0/go.mod h1:0bklkJhTG7nC6AVsdDi0wt5bGoqvzdZSzMMQkilZ6XM=
-github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
-github.com/emicklei/go-restful/v3 v3.12.2/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/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
+github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
 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=
@@ -91,6 +91,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
 github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
 github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
+github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
+github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
+github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
+github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/T/4neo=
+github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -98,12 +104,12 @@ 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.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/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
+github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
 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-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
+github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
@@ -143,8 +149,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
 github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
 github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
@@ -155,6 +161,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
+github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -169,8 +177,12 @@ 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=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
+github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
+github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
+github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
+github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -196,8 +208,8 @@ github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
 github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
-github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
+github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
+github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
 github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
 github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -215,10 +227,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
-github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
-github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
-github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
+github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
+github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
+github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
 github.com/prometheus/prometheus v0.306.0 h1:Q0Pvz/ZKS6vVWCa1VSgNyNJlEe8hxdRlKklFg7SRhNw=
 github.com/prometheus/prometheus v0.306.0/go.mod h1:7hMSGyZHt0dcmZ5r4kFPJ/vxPQU99N5/BGwSPDxeZrQ=
 github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk=
@@ -244,6 +256,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -252,22 +272,24 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
-go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
-go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
-go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
-go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
-go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
-go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -278,8 +300,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
-go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -299,8 +321,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
 golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
+golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -339,19 +361,19 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
 gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
 google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
 google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
-google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
-google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
-google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
-google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
-google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
+google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
+google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
-gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 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=
@@ -375,24 +397,24 @@ k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc=
 k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg=
 k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
 k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
-k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q=
-k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
+k8s.io/gengo/v2 v2.0.0-20250820003526-c297c0c1eb9d h1:qUrYOinhdAUL0xxhA4gPqogPBaS9nIq2l2kTb6pmeB0=
+k8s.io/gengo/v2 v2.0.0-20250820003526-c297c0c1eb9d/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
-k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
-k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
-k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE=
+k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
+k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
+k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
-sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
-sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
+sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
+sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
 sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk=
 sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0=
-sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
-sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
-sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ=
+sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
 sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
 sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
diff --git a/mkdocs.yml b/mkdocs.yml
index b746f74..f2807cb 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -74,6 +74,7 @@ nav:
       - Configuration Guide:
           - Configuring the plugins via configuration YAML file: guides/epp-configuration/config-text.md
           - Prefix Cache Aware Plugin: guides/epp-configuration/prefix-aware.md
+      - Migration Guide: guides/ga-migration.md
       - Troubleshooting Guide: guides/troubleshooting.md
     - Implementer Guides:
       - Getting started: guides/implementers.md
@@ -82,11 +83,15 @@ nav:
     - Benchmark: performance/benchmark/index.md
     - Regression Testing: performance/regression-testing/index.md
   - Reference:
-    - API Reference: reference/spec.md
-    - Alpha API Reference: reference/x-spec.md
+    - v1 API Reference: reference/spec.md
+    - v1alpha1 API Reference:
+      - reference/x-v1a1-spec.md
+    - v1alpha2 API Reference:
+      - reference/x-v1a2-spec.md
     - API Types:
       - InferencePool: api-types/inferencepool.md
       - InferenceObjective: api-types/inferenceobjective.md
+      - InferencePoolImport: api-types/inferencepoolimport.md
   - Enhancements:
     - Overview: enhancements/overview.md
   - Contributing:
diff --git a/pkg/common/telemetry.go b/pkg/common/telemetry.go
new file mode 100644
index 0000000..3723e0f
--- /dev/null
+++ b/pkg/common/telemetry.go
@@ -0,0 +1,140 @@
+/*
+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 common
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/go-logr/logr"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
+	"go.opentelemetry.io/otel/propagation"
+	"go.opentelemetry.io/otel/sdk/resource"
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+	"sigs.k8s.io/gateway-api-inference-extension/version"
+)
+
+type errorHandler struct {
+	logger logr.Logger
+}
+
+func (h *errorHandler) Handle(err error) {
+	h.logger.V(logging.DEFAULT).Error(err, "trace error occurred")
+}
+
+func InitTracing(ctx context.Context, logger logr.Logger) error {
+	logger = logger.WithName("trace")
+	loggerWrap := &errorHandler{logger: logger}
+
+	_, ok := os.LookupEnv("OTEL_SERVICE_NAME")
+	if !ok {
+		os.Setenv("OTEL_SERVICE_NAME", "gateway-api-inference-extension")
+	}
+
+	_, ok = os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
+	if !ok {
+		os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
+	}
+
+	traceExporter, err := initTraceExporter(ctx, logger)
+	if err != nil {
+		loggerWrap.Handle(fmt.Errorf("%s: %v", "init trace exporter failed", err))
+		return err
+	}
+
+	// Go SDK doesn't have an automatic sampler, handle manually
+	samplerType, ok := os.LookupEnv("OTEL_TRACES_SAMPLER")
+	if !ok {
+		samplerType = "parentbased_traceidratio"
+	}
+	samplerARG, ok := os.LookupEnv("OTEL_TRACES_SAMPLER_ARG")
+	if !ok {
+		samplerARG = "0.1"
+	}
+
+	sampler := sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))
+	if samplerType == "parentbased_traceidratio" {
+		fraction, err := strconv.ParseFloat(samplerARG, 64)
+		if err != nil {
+			fraction = 0.1
+		}
+
+		sampler = sdktrace.ParentBased(sdktrace.TraceIDRatioBased(fraction))
+	} else {
+		loggerWrap.Handle(fmt.Errorf("unsupported sampler type: %s, fallback to parentbased_traceidratio with 0.1 Ratio", samplerType))
+	}
+
+	opt := []sdktrace.TracerProviderOption{
+		sdktrace.WithBatcher(traceExporter),
+		sdktrace.WithSampler(sampler),
+		sdktrace.WithResource(resource.NewWithAttributes(
+			semconv.SchemaURL,
+			semconv.ServiceVersionKey.String(version.BuildRef),
+		)),
+	}
+
+	tracerProvider := sdktrace.NewTracerProvider(opt...)
+	otel.SetTracerProvider(tracerProvider)
+	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
+	otel.SetErrorHandler(loggerWrap)
+
+	go func() {
+		<-ctx.Done()
+		err := tracerProvider.Shutdown(context.Background())
+		if err != nil {
+			loggerWrap.Handle(fmt.Errorf("%s: %v", "failed to shutdown TraceProvider", err))
+		}
+
+		logger.V(logging.DEFAULT).Info("trace provider shutting down")
+	}()
+
+	return nil
+}
+
+// initTraceExporter create a SpanExporter
+// support exporter type
+// - console: export spans in console for development use case
+// - otlp: export spans through gRPC to an opentelemetry collector
+func initTraceExporter(ctx context.Context, logger logr.Logger) (sdktrace.SpanExporter, error) {
+	var traceExporter sdktrace.SpanExporter
+	traceExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
+	if err != nil {
+		return nil, fmt.Errorf("failed to create stdouttrace exporter: %w", err)
+	}
+
+	exporterType, ok := os.LookupEnv("OTEL_TRACES_EXPORTER")
+	if !ok {
+		exporterType = "console"
+	}
+
+	logger.Info("init OTel trace exporter", "type", exporterType)
+	if exporterType == "otlp" {
+		traceExporter, err = otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure())
+		if err != nil {
+			return nil, fmt.Errorf("failed to create otlp-grcp exporter: %w", err)
+		}
+	}
+
+	return traceExporter, nil
+}
diff --git a/pkg/epp/flowcontrol/controller/controller.go b/pkg/epp/flowcontrol/controller/controller.go
index aeb0bdd..93d0330 100644
--- a/pkg/epp/flowcontrol/controller/controller.go
+++ b/pkg/epp/flowcontrol/controller/controller.go
@@ -28,15 +28,18 @@ import (
 	"errors"
 	"fmt"
 	"slices"
+	"strconv"
 	"sync"
 	"time"
 
 	"github.com/go-logr/logr"
+	k8srand "k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/utils/clock"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/contracts"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/controller/internal"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
@@ -57,10 +60,11 @@ type shardProcessor interface {
 // shardProcessorFactory defines the signature for a function that creates a `shardProcessor`.
 // This enables dependency injection for testing.
 type shardProcessorFactory func(
+	ctx context.Context,
 	shard contracts.RegistryShard,
 	saturationDetector contracts.SaturationDetector,
-	clock clock.Clock,
-	expiryCleanupInterval time.Duration,
+	clock clock.WithTicker,
+	cleanupSweepInterval time.Duration,
 	enqueueChannelBufferSize int,
 	logger logr.Logger,
 ) shardProcessor
@@ -79,6 +83,14 @@ type managedWorker struct {
 //
 // The controller's `Run` loop executes periodically, acting as a garbage collector that keeps the pool of running
 // workers synchronized with the dynamic shard topology of the `FlowRegistry`.
+//
+// Request Lifecycle Management:
+//
+//  1. Asynchronous Finalization (Controller-Owned): The Controller actively monitors the request Context
+//     (TTL/Cancellation) in EnqueueAndWait. If the Context expires, the Controller immediately Finalizes the item and
+//     unblocks the caller.
+//  2. Synchronous Finalization (Processor-Owned): The Processor handles Dispatch, Capacity Rejection, and Shutdown.
+//  3. Cleanup (Processor-Owned): The Processor periodically sweeps externally finalized items to reclaim capacity.
 type FlowController struct {
 	// --- Immutable dependencies (set at construction) ---
 
@@ -129,18 +141,20 @@ func NewFlowController(
 
 	// Use the real shard processor implementation by default.
 	fc.shardProcessorFactory = func(
+		ctx context.Context,
 		shard contracts.RegistryShard,
 		saturationDetector contracts.SaturationDetector,
-		clock clock.Clock,
-		expiryCleanupInterval time.Duration,
+		clock clock.WithTicker,
+		cleanupSweepInterval time.Duration,
 		enqueueChannelBufferSize int,
 		logger logr.Logger,
 	) shardProcessor {
 		return internal.NewShardProcessor(
+			ctx,
 			shard,
 			saturationDetector,
 			clock,
-			expiryCleanupInterval,
+			cleanupSweepInterval,
 			enqueueChannelBufferSize,
 			logger)
 	}
@@ -189,63 +203,167 @@ func (fc *FlowController) run(ctx context.Context) {
 //     stack and its `context.Context`. The system only needs to signal this specific goroutine to unblock it.
 //   - Direct Backpressure: If queues are full, `EnqueueAndWait` returns an error immediately, providing direct
 //     backpressure to the caller.
-func (fc *FlowController) EnqueueAndWait(req types.FlowControlRequest) (types.QueueOutcome, error) {
+func (fc *FlowController) EnqueueAndWait(
+	ctx context.Context,
+	req types.FlowControlRequest,
+) (types.QueueOutcome, error) {
 	if req == nil {
 		return types.QueueOutcomeRejectedOther, errors.New("request cannot be nil")
 	}
-	effectiveTTL := req.InitialEffectiveTTL()
-	if effectiveTTL <= 0 {
-		effectiveTTL = fc.config.DefaultRequestTTL
-	}
-	enqueueTime := fc.clock.Now()
 
+	flowKey := req.FlowKey()
+	fairnessID := flowKey.ID
+	priority := strconv.Itoa(flowKey.Priority)
+	metrics.IncFlowControlQueueSize(fairnessID, priority)
+	defer metrics.DecFlowControlQueueSize(fairnessID, priority)
+
+	// 1. Create the derived context that governs this request's lifecycle (Parent Cancellation + TTL).
+	reqCtx, cancel, enqueueTime := fc.createRequestContext(ctx, req)
+	defer cancel()
+
+	// 2. Enter the distribution loop to find a home for the request.
+	// This loop is responsible for retrying on ErrShardDraining.
 	for {
-		select {
+		select { // Non-blocking check on controller lifecycle.
 		case <-fc.parentCtx.Done():
 			return types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, types.ErrFlowControllerNotRunning)
 		default:
-			// The controller is running, proceed.
 		}
 
-		// We must create a fresh `FlowItem` on each attempt since finalization is idempotent.
-		// However, we use the original, preserved `enqueueTime`.
-		item := internal.NewItem(req, effectiveTTL, enqueueTime)
-		if outcome, err := fc.distributeRequest(item); err != nil {
-			return outcome, fmt.Errorf("%w: %w", types.ErrRejected, err)
+		// Attempt to distribute the request once.
+		item, err := fc.tryDistribution(reqCtx, req, enqueueTime)
+		if err != nil {
+			// Distribution failed terminally (e.g., no shards, context cancelled during blocking submit).
+			// The item has already been finalized by tryDistribution.
+			finalState := item.FinalState()
+			return finalState.Outcome, finalState.Err
 		}
 
-		// Block until the request is finalized (dispatched, rejected, or evicted).
-		// The finalization logic internally monitors for context cancellation and TTL expiry.
-		finalState := <-item.Done()
-		if errors.Is(finalState.Err, contracts.ErrShardDraining) {
-			fc.logger.V(logutil.DEBUG).Info("Shard is draining, retrying request", "requestID", req.ID())
-			// Benign race with the chosen `contracts.RegistryShard` becoming Draining post selection but before the item was
-			// enqueued into its respective `contracts.ManagedQueue`. Simply try again.
+		// Distribution was successful; ownership of the item has been transferred to a processor.
+		// Now, we block here in awaitFinalization until the request is finalized by either the processor (e.g., dispatched,
+		// rejected) or the controller itself (e.g., caller's context cancelled/TTL expired).
+		outcome, err := fc.awaitFinalization(reqCtx, item)
+		if errors.Is(err, contracts.ErrShardDraining) {
+			// This is a benign race condition where the chosen shard started draining after acceptance.
+			fc.logger.V(logutil.DEBUG).Info("Selected shard is Draining, retrying request distribution",
+				"flowKey", req.FlowKey(), "requestID", req.ID())
+			// Introduce a small, randomized delay (1-10ms) to prevent tight spinning loops and thundering herds during retry
+			// scenarios (e.g., shard draining)
+			// TODO: Replace this with a more sophisticated backoff strategy when our data parallelism story matures.
+			// For now, this is more than sufficient.
+			jitterMs := k8srand.Intn(10) + 1
+			fc.clock.Sleep(time.Duration(jitterMs) * time.Millisecond)
 			continue
 		}
 
+		// The outcome is terminal (Dispatched, Evicted, or a non-retriable rejection).
+		return outcome, err
+	}
+}
+
+var errNoShards = errors.New("no viable active shards available")
+
+// tryDistribution handles a single attempt to select a shard and submit a request.
+// If this function returns an error, it guarantees that the provided `item` has been finalized.
+func (fc *FlowController) tryDistribution(
+	reqCtx context.Context,
+	req types.FlowControlRequest,
+	enqueueTime time.Time,
+) (*internal.FlowItem, error) {
+	// Calculate effective TTL for item initialization (reqCtx is the enforcement mechanism).
+	effectiveTTL := fc.config.DefaultRequestTTL
+	if deadline, ok := reqCtx.Deadline(); ok {
+		if ttl := deadline.Sub(enqueueTime); ttl > 0 {
+			effectiveTTL = ttl
+		}
+	}
+
+	// We must create a fresh FlowItem on each attempt as finalization is per-lifecycle.
+	item := internal.NewItem(req, effectiveTTL, enqueueTime)
+
+	candidates, err := fc.selectDistributionCandidates(item.OriginalRequest().FlowKey())
+	if err != nil {
+		outcome := types.QueueOutcomeRejectedOther
+		if errors.Is(err, errNoShards) {
+			outcome = types.QueueOutcomeRejectedCapacity
+		}
+		finalErr := fmt.Errorf("%w: request not accepted: %w", types.ErrRejected, err)
+		item.FinalizeWithOutcome(outcome, finalErr)
+		return item, finalErr
+	}
+
+	outcome, err := fc.distributeRequest(reqCtx, item, candidates)
+	if err == nil {
+		// Success: Ownership of the item has been transferred to the processor.
+		return item, nil
+	}
+
+	// For any distribution error, the controller retains ownership and must finalize the item.
+	var finalErr error
+	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+		// We propagate the original context error here, EnqueueAndWait will rely on item.FinalState().Err.
+		finalErr = err
+		item.Finalize(context.Cause(reqCtx))
+	} else { // e.g.,
+		finalErr = fmt.Errorf("%w: request not accepted: %w", types.ErrRejected, err)
+		item.FinalizeWithOutcome(outcome, finalErr)
+	}
+	return item, finalErr
+}
+
+// awaitFinalization blocks until an item is finalized, either by the processor (synchronously) or by the controller
+// itself due to context expiry (asynchronously).
+func (fc *FlowController) awaitFinalization(
+	reqCtx context.Context,
+	item *internal.FlowItem,
+) (types.QueueOutcome, error) {
+	select {
+	case <-reqCtx.Done():
+		// Asynchronous Finalization (Controller-initiated):
+		// The request Context expired (Cancellation/TTL) while the item was being processed.
+		cause := context.Cause(reqCtx)
+		item.Finalize(cause)
+
+		// The processor will eventually discard this "zombie" item during its cleanup sweep.
+		finalState := item.FinalState()
+		return finalState.Outcome, finalState.Err
+
+	case finalState := <-item.Done():
+		// Synchronous Finalization (Processor-initiated):
+		// The processor finalized the item (Dispatch, Reject, Shutdown).
 		return finalState.Outcome, finalState.Err
 	}
 }
 
-// distributeRequest implements a flow-aware, two-phase "Join-Shortest-Queue-by-Bytes" (JSQ-Bytes) distribution strategy
-// with graceful backpressure. It selects the optimal worker for a given item and attempts to submit it.
-//
-// The algorithm operates as follows:
-//  1. Candidate Selection: It identifies all Active shards for the item's flow and ranks them by the current byte size
-//     of that flow's queue, from least to most loaded.
-//  2. Phase 1 (Non-blocking Fast Failover): It iterates through the ranked candidates and attempts a non-blocking
-//     submission. The first successful submission wins.
-//  3. Phase 2 (Blocking Fallback): If all non-blocking attempts fail, it performs a single blocking submission to the
-//     least-loaded candidate, providing backpressure.
-func (fc *FlowController) distributeRequest(item *internal.FlowItem) (types.QueueOutcome, error) {
-	key := item.OriginalRequest().FlowKey()
-	reqID := item.OriginalRequest().ID()
-	type candidate struct {
-		processor shardProcessor
-		shardID   string
-		byteSize  uint64
+// createRequestContext derives the context that governs a request's lifecycle, enforcing the TTL deadline.
+func (fc *FlowController) createRequestContext(
+	ctx context.Context,
+	req types.FlowControlRequest,
+) (context.Context, context.CancelFunc, time.Time) {
+	enqueueTime := fc.clock.Now()
+	effectiveTTL := req.InitialEffectiveTTL()
+	if effectiveTTL <= 0 {
+		effectiveTTL = fc.config.DefaultRequestTTL
 	}
+
+	if effectiveTTL > 0 {
+		reqCtx, cancel := context.WithDeadlineCause(ctx, enqueueTime.Add(effectiveTTL), types.ErrTTLExpired)
+		return reqCtx, cancel, enqueueTime
+	}
+	reqCtx, cancel := context.WithCancel(ctx)
+	return reqCtx, cancel, enqueueTime
+}
+
+// candidate holds the information needed to evaluate a shard as a potential target for a request.
+type candidate struct {
+	processor shardProcessor
+	shardID   string
+	byteSize  uint64
+}
+
+// selectDistributionCandidates identifies all Active shards for the item's flow and ranks them by the current byte size
+// of that flow's queue, from least to most loaded.
+func (fc *FlowController) selectDistributionCandidates(key types.FlowKey) ([]candidate, error) {
 	var candidates []candidate
 	err := fc.registry.WithConnection(key, func(conn contracts.ActiveFlowConnection) error {
 		shards := conn.ActiveShards()
@@ -262,41 +380,58 @@ func (fc *FlowController) distributeRequest(item *internal.FlowItem) (types.Queu
 		return nil
 	})
 	if err != nil {
-		return types.QueueOutcomeRejectedOther, fmt.Errorf("failed to acquire lease for request %q (flow %s): %w",
-			reqID, key, err)
+		return nil, fmt.Errorf("failed to acquire lease for flow %s: %w", key, err)
 	}
 
 	if len(candidates) == 0 {
-		return types.QueueOutcomeRejectedCapacity, fmt.Errorf("no viable Active shards available for request %q (flow %s)",
-			reqID, key)
+		return nil, fmt.Errorf("%w for flow %s", errNoShards, key)
 	}
 
 	slices.SortFunc(candidates, func(a, b candidate) int {
 		return cmp.Compare(a.byteSize, b.byteSize)
 	})
 
-	// --- Phase 1: Fast, non-blocking failover attempt ---
+	return candidates, nil
+}
+
+// distributeRequest implements a flow-aware, two-phase "Join-Shortest-Queue-by-Bytes" (JSQ-Bytes) distribution strategy
+// with graceful backpressure. It attempts to submit an item to the best-ranked candidate from the provided list.
+//
+// The algorithm operates as follows:
+//  1. Phase 1 (Non-blocking Fast Failover): It iterates through the ranked candidates and attempts a non-blocking
+//     submission. The first successful submission wins.
+//  2. Phase 2 (Blocking Fallback): If all non-blocking attempts fail, it performs a single blocking submission to the
+//     least-loaded candidate, providing backpressure.
+//
+// The provided context (ctx) is used for the blocking submission phase (SubmitOrBlock).
+//
+// Ownership Contract:
+//   - Returns nil: Success. Ownership transferred to Processor.
+//   - Returns error: Failure (Context expiry, shutdown,, etc.).
+//     Ownership retained by Controller. The Controller MUST finalize the item.
+func (fc *FlowController) distributeRequest(
+	ctx context.Context,
+	item *internal.FlowItem,
+	candidates []candidate,
+) (types.QueueOutcome, error) {
+	reqID := item.OriginalRequest().ID()
 	for _, c := range candidates {
 		if err := c.processor.Submit(item); err == nil {
-			return types.QueueOutcomeNotYetFinalized, nil // Success
+			return types.QueueOutcomeNotYetFinalized, nil
 		}
-		fc.logger.V(logutil.DEBUG).Info("Processor busy during fast failover, trying next candidate",
+		fc.logger.V(logutil.TRACE).Info("Processor busy during fast failover, trying next candidate",
 			"shardID", c.shardID, "requestID", reqID)
 	}
 
-	// --- Phase 2: All processors busy. Attempt a single blocking send to the best candidate. ---
+	// All processors are busy. Attempt a single blocking submission to the least-loaded candidate.
 	bestCandidate := candidates[0]
-	fc.logger.V(logutil.DEBUG).Info("All processors busy, attempting blocking submit to best candidate",
-		"shardID", bestCandidate.shardID, "requestID", reqID, "queueByteSize", bestCandidate.byteSize)
-
-	err = bestCandidate.processor.SubmitOrBlock(item.OriginalRequest().Context(), item)
+	fc.logger.V(logutil.TRACE).Info("All processors busy, attempting blocking submit to best candidate",
+		"shardID", bestCandidate.shardID, "requestID", reqID)
+	err := bestCandidate.processor.SubmitOrBlock(ctx, item)
 	if err != nil {
-		// If even the blocking attempt fails (e.g., context cancelled or processor shut down), the request is definitively
-		// rejected.
-		return types.QueueOutcomeRejectedCapacity, fmt.Errorf(
-			"all viable shard processors are at capacity for request %q (flow %s): %w", reqID, key, err)
+		return types.QueueOutcomeRejectedOther, fmt.Errorf("%w: request not accepted: %w", types.ErrRejected, err)
 	}
-	return types.QueueOutcomeNotYetFinalized, nil
+	return types.QueueOutcomeNotYetFinalized, nil // Success, ownership transferred.
 }
 
 // getOrStartWorker implements the lazy-loading and startup of shard processors.
@@ -311,6 +446,7 @@ func (fc *FlowController) getOrStartWorker(shard contracts.RegistryShard) *manag
 	// Construct a new worker, but do not start its processor goroutine yet.
 	processorCtx, cancel := context.WithCancel(fc.parentCtx)
 	processor := fc.shardProcessorFactory(
+		processorCtx,
 		shard,
 		fc.saturationDetector,
 		fc.clock,
diff --git a/pkg/epp/flowcontrol/controller/internal/item.go b/pkg/epp/flowcontrol/controller/internal/item.go
index d5bdaaf..f0d5d32 100644
--- a/pkg/epp/flowcontrol/controller/internal/item.go
+++ b/pkg/epp/flowcontrol/controller/internal/item.go
@@ -17,97 +17,177 @@ limitations under the License.
 package internal
 
 import (
+	"context"
+	"errors"
+	"fmt"
+	"strconv"
 	"sync"
+	"sync/atomic"
 	"time"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 )
 
-// FinalState encapsulates the terminal outcome of a `FlowItem`'s lifecycle.
-// It is sent over the item's `Done()` channel exactly once.
+// FinalState encapsulates the terminal outcome of a FlowItem's lifecycle.
 type FinalState struct {
 	Outcome types.QueueOutcome
 	Err     error
 }
 
-// FlowItem is the internal representation of a request managed by the `FlowController`.
+// FlowItem is the internal representation of a request managed by the Flow Controller.
+//
+// # Lifecycle Management
+//
+// Finalization (determining outcome) can be initiated by the Controller (e.g., Context expiry) or the Processor (e.g.,
+// Dispatch/Reject). It sets the outcome and signals the waiting goroutine.
+//
+// # Synchronization
+//
+// Atomic operations synchronize state across the Controller and Processor goroutines:
+//   - finalState (atomic.Pointer): Safely publishes the outcome.
+//   - handle (atomic.Pointer): Safely publishes the queue admission status.
 type FlowItem struct {
-	// --- Immutable fields (set at creation) ---
+	// --- Immutable fields during a single lifecycle ---
 
 	enqueueTime     time.Time
 	effectiveTTL    time.Duration
 	originalRequest types.FlowControlRequest
-	handle          types.QueueItemHandle
 
-	// --- Finalization state (protected by onceFinalize) ---
+	// --- Synchronized State ---
 
-	// done is closed exactly once when the item is finalized.
-	// The closing of this channel establishes a "happens-before" memory barrier, guaranteeing that writes to `outcome`
-	// and `err` are visible to any goroutine that has successfully read from `done`.
-	done chan FinalState
+	// handle stores the types.QueueItemHandle atomically.
+	// Written by the Processor (SetHandle) when admitted.
+	// Read by inferOutcome (called by Finalize) to infer the outcome (Rejected vs. Evicted).
+	// Distinguishing between pre-admission (Rejection) and post-admission (Eviction) during asynchronous finalization
+	// relies on whether this handle is nil or non-nil.
+	handle atomic.Pointer[types.QueueItemHandle]
 
-	// finalState  is safely visible to any goroutine after it has confirmed the channel is closed.
-	finalState FinalState
+	// finalState holds the result of the finalization. Stored atomically once.
+	// Use FinalState() for safe access.
+	finalState atomic.Pointer[FinalState]
 
-	// onceFinalize ensures the `finalize()` logic is idempotent.
+	// --- Finalization Signaling ---
+
+	// done is the channel used to signal the completion of the item's lifecycle.
+	// Buffered to size 1 to prevent Finalize from blocking.
+	done chan *FinalState
+
+	// onceFinalize ensures the finalization logic runs exactly once per lifecycle.
 	onceFinalize sync.Once
 }
 
-// ensure FlowItem implements the interface.
 var _ types.QueueItemAccessor = &FlowItem{}
 
-// NewItem creates a new `FlowItem`.
+// NewItem allocates and initializes a new FlowItem for a request lifecycle.
 func NewItem(req types.FlowControlRequest, effectiveTTL time.Duration, enqueueTime time.Time) *FlowItem {
 	return &FlowItem{
 		enqueueTime:     enqueueTime,
 		effectiveTTL:    effectiveTTL,
 		originalRequest: req,
-		// Buffer to size one, preventing finalizing goroutine (e.g., the dispatcher) from blocking if the waiting
-		// goroutine has already timed out and is no longer reading.
-		done: make(chan FinalState, 1),
+		done:            make(chan *FinalState, 1),
 	}
 }
 
-// EnqueueTime returns the time the item was logically accepted by the `FlowController` for queuing. This is used as the
-// basis for TTL calculations.
+// EnqueueTime returns the time the item was logically accepted by the FlowController.
 func (fi *FlowItem) EnqueueTime() time.Time { return fi.enqueueTime }
 
-// EffectiveTTL returns the actual time-to-live assigned to this item by the `FlowController`.
+// EffectiveTTL returns the actual time-to-live assigned to this item.
 func (fi *FlowItem) EffectiveTTL() time.Duration { return fi.effectiveTTL }
 
-// OriginalRequest returns the original, underlying `types.FlowControlRequest` object.
+// OriginalRequest returns the original types.FlowControlRequest object.
 func (fi *FlowItem) OriginalRequest() types.FlowControlRequest { return fi.originalRequest }
 
-// Handle returns the `types.QueueItemHandle` that uniquely identifies this item within a specific queue instance. It
-// returns nil if the item has not yet been added to a queue.
-func (fi *FlowItem) Handle() types.QueueItemHandle { return fi.handle }
+// Done returns a read-only channel that will receive the FinalState pointer exactly once.
+func (fi *FlowItem) Done() <-chan *FinalState { return fi.done }
 
-// SetHandle associates a `types.QueueItemHandle` with this item. This method is called by a `framework.SafeQueue`
-// implementation immediately after the item is added to the queue.
-func (fi *FlowItem) SetHandle(handle types.QueueItemHandle) { fi.handle = handle }
+// FinalState returns the FinalState if the item has been finalized, or nil otherwise.
+// Safe for concurrent access.
+func (fi *FlowItem) FinalState() *FinalState { return fi.finalState.Load() }
 
-// Done returns a channel that is closed when the item has been finalized (e.g., dispatched, rejected, or evicted).
-func (fi *FlowItem) Done() <-chan FinalState {
-	return fi.done
+// Handle returns the types.QueueItemHandle for this item within a queue.
+// Returns nil if the item is not in a queue. Safe for concurrent access.
+func (fi *FlowItem) Handle() types.QueueItemHandle {
+	ptr := fi.handle.Load()
+	if ptr == nil {
+		return nil
+	}
+	return *ptr
 }
 
-// Finalize sets the item's terminal state and signals the waiting goroutine by closing its `done` channel idempotently.
-// This method is idempotent and is the single point where an item's lifecycle concludes.
-// It is intended to be called only by the component that owns the item's lifecycle, such as a `ShardProcessor`.
-func (fi *FlowItem) Finalize(outcome types.QueueOutcome, err error) {
+// SetHandle associates a types.QueueItemHandle with this item. Called by the queue implementation (via Processor).
+// Safe for concurrent access.
+func (fi *FlowItem) SetHandle(handle types.QueueItemHandle) { fi.handle.Store(&handle) }
+
+// Finalize determines the item's terminal state based on the provided cause (e.g., Context error) and the item's
+// current admission status (queued or not).
+//
+// This method is intended for asynchronous finalization initiated by the Controller (e.g., TTL expiry).
+// It is idempotent.
+func (fi *FlowItem) Finalize(cause error) {
 	fi.onceFinalize.Do(func() {
-		finalState := FinalState{Outcome: outcome, Err: err}
-		fi.finalState = finalState
-		fi.done <- finalState
-		close(fi.done)
+		// Atomically load the handle to determine if the item was admitted to a queue.
+		// This synchronization is critical for correctly inferring the outcome across goroutines.
+		isQueued := fi.Handle() != nil
+		outcome, finalErr := inferOutcome(cause, isQueued)
+		fi.finalizeInternal(outcome, finalErr)
 	})
 }
 
-// isFinalized checks if the item has been finalized without blocking or consuming the final state.
-// It is a side-effect-free check used by the `ShardProcessor` as a defensive measure to avoid operating on
-// already-completed items.
-func (fi *FlowItem) isFinalized() bool {
-	// A buffered channel of size 1 can be safely and non-blockingly checked by its length.
-	// If the finalize function has run, it will have sent a value, and the length will be 1.
-	return len(fi.done) > 0
+// FinalizeWithOutcome sets the item's terminal state explicitly.
+//
+// This method is intended for synchronous finalization by the Processor (Dispatch, Reject) or the Controller
+// (Distribution failure).
+// It is idempotent.
+func (fi *FlowItem) FinalizeWithOutcome(outcome types.QueueOutcome, err error) {
+	fi.onceFinalize.Do(func() {
+		fi.finalizeInternal(outcome, err)
+	})
+}
+
+// finalizeInternal is the core finalization logic. It must be called within the sync.Once.Do block.
+// It captures the state, stores it atomically, and signals the Done channel.
+func (fi *FlowItem) finalizeInternal(outcome types.QueueOutcome, err error) {
+	finalState := &FinalState{
+		Outcome: outcome,
+		Err:     err,
+	}
+
+	// Atomically store the pointer. This is the critical memory barrier that publishes the state safely.
+	fi.finalState.Store(finalState)
+
+	duration := time.Since(fi.enqueueTime)
+	flowKey := fi.originalRequest.FlowKey()
+	metrics.RecordFlowControlRequestQueueDuration(flowKey.ID, strconv.Itoa(flowKey.Priority), outcome.String(), duration)
+
+	fi.done <- finalState
+	close(fi.done)
+}
+
+// inferOutcome determines the correct QueueOutcome and Error based on the cause of finalization and whether the item
+// was already admitted to a queue.
+func inferOutcome(cause error, isQueued bool) (types.QueueOutcome, error) {
+	var specificErr error
+	var outcomeIfEvicted types.QueueOutcome
+	switch {
+	case errors.Is(cause, types.ErrTTLExpired) || errors.Is(cause, context.DeadlineExceeded):
+		specificErr = types.ErrTTLExpired
+		outcomeIfEvicted = types.QueueOutcomeEvictedTTL
+	case errors.Is(cause, context.Canceled):
+		specificErr = fmt.Errorf("%w: %w", types.ErrContextCancelled, cause)
+		outcomeIfEvicted = types.QueueOutcomeEvictedContextCancelled
+	default:
+		// Handle other potential causes (e.g., custom context errors).
+		specificErr = cause
+		outcomeIfEvicted = types.QueueOutcomeEvictedOther
+	}
+
+	if isQueued {
+		// The item was in the queue when it expired/cancelled.
+		return outcomeIfEvicted, fmt.Errorf("%w: %w", types.ErrEvicted, specificErr)
+	}
+
+	// The item was not yet in the queue (e.g., buffered in enqueueChan).
+	// We treat this as a rejection, as it never formally consumed queue capacity.
+	return types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, specificErr)
 }
diff --git a/pkg/epp/flowcontrol/controller/internal/processor.go b/pkg/epp/flowcontrol/controller/internal/processor.go
index 9b6bb70..e3cccc1 100644
--- a/pkg/epp/flowcontrol/controller/internal/processor.go
+++ b/pkg/epp/flowcontrol/controller/internal/processor.go
@@ -27,7 +27,6 @@ import (
 
 	"github.com/go-logr/logr"
 	"k8s.io/utils/clock"
-	"sigs.k8s.io/controller-runtime/pkg/log"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/contracts"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
@@ -44,106 +43,116 @@ const maxCleanupWorkers = 4
 // This is used as a signal for the `controller.FlowController`'s "fast failover" logic.
 var ErrProcessorBusy = errors.New("shard processor is busy")
 
-// ShardProcessor is the core worker of the `controller.FlowController`.
-// It is paired one-to-one with a `contracts.RegistryShard` instance and is responsible for all request lifecycle
-// operations on that shard. It acts as the "data plane" worker that executes against the concurrent-safe state provided
-// by its shard.
+// ShardProcessor is the core worker of the FlowController.
 //
-// # Concurrency Model: The Single-Writer Actor
+// It is paired one-to-one with a RegistryShard instance and is responsible for all request lifecycle operations on that
+// shard, from the point an item is successfully submitted to it.
 //
-// To ensure correctness and high performance, the processor uses a single-goroutine, actor-based model. The main `Run`
-// loop is the sole "writer" for all state-mutating operations, particularly enqueueing. This makes complex transactions
-// inherently atomic without coarse-grained locks.
+// # Request Lifecycle Management & Ownership
+//
+// The ShardProcessor takes ownership of a FlowItem only after it has been successfully sent to its internal enqueueChan
+// via Submit or SubmitOrBlock (i.e., when these methods return nil).
+// Once the Processor takes ownership, it is solely responsible for ensuring that item.Finalize() or
+// item.FinalizeWithOutcome() is called exactly once for that item, under all circumstances (dispatch, rejection, sweep,
+// or shutdown).
+//
+// If Submit or SubmitOrBlock return an error, ownership remains with the caller (the Controller), which must then
+// handle the finalization.
 //
-// # Concurrency Guarantees
+// # Concurrency Model
 //
-//  1. Safe Enqueueing: The "check-then-act" sequence for capacity is safe because it is only ever performed by the
-//     single `Run` goroutine.
-//  2. Idempotent Finalization: The primary internal race condition is between the main `dispatchCycle` and the
-//     background `runExpiryCleanup` goroutine, both of which might try to finalize an item. This is resolved by the
-//     `FlowItem.Finalize` method, which uses `sync.Once` to guarantee that only the first attempt to finalize an item
-//     succeeds.
+// To ensure correctness and high performance, the processor uses a single-goroutine, actor-based model. The main run
+// loop is the sole writer for all state-mutating operations. This makes complex transactions (like capacity checks)
+// inherently atomic without coarse-grained locks.
 type ShardProcessor struct {
-	shard                 contracts.RegistryShard
-	saturationDetector    contracts.SaturationDetector
-	clock                 clock.Clock
-	expiryCleanupInterval time.Duration
-	logger                logr.Logger
+	shard                contracts.RegistryShard
+	saturationDetector   contracts.SaturationDetector
+	clock                clock.WithTicker
+	cleanupSweepInterval time.Duration
+	logger               logr.Logger
+
+	// lifecycleCtx controls the processor's lifetime. Monitored by Submit* methods for safe shutdown.
+	lifecycleCtx context.Context
 
-	// enqueueChan is the entry point for new requests to be processed by this shard's `Run` loop.
+	// enqueueChan is the entry point for new requests.
 	enqueueChan chan *FlowItem
-	// wg is used to wait for background tasks like expiry cleanup to complete on shutdown.
+
+	// wg is used to wait for background tasks (cleanup sweep) to complete on shutdown.
 	wg             sync.WaitGroup
 	isShuttingDown atomic.Bool
 	shutdownOnce   sync.Once
 }
 
-// NewShardProcessor creates a new `ShardProcessor` instance.
+// NewShardProcessor creates a new ShardProcessor instance.
 func NewShardProcessor(
+	ctx context.Context,
 	shard contracts.RegistryShard,
 	saturationDetector contracts.SaturationDetector,
-	clock clock.Clock,
-	expiryCleanupInterval time.Duration,
+	clock clock.WithTicker,
+	cleanupSweepInterval time.Duration,
 	enqueueChannelBufferSize int,
 	logger logr.Logger,
 ) *ShardProcessor {
 	return &ShardProcessor{
-		shard:                 shard,
-		saturationDetector:    saturationDetector,
-		clock:                 clock,
-		expiryCleanupInterval: expiryCleanupInterval,
-		logger:                logger,
-		// A buffered channel decouples the processor from the distributor, allowing for a fast, asynchronous handoff of new
-		// requests.
-		enqueueChan: make(chan *FlowItem, enqueueChannelBufferSize),
+		shard:                shard,
+		saturationDetector:   saturationDetector,
+		clock:                clock,
+		cleanupSweepInterval: cleanupSweepInterval,
+		logger:               logger,
+		lifecycleCtx:         ctx,
+		enqueueChan:          make(chan *FlowItem, enqueueChannelBufferSize),
 	}
 }
 
-// Submit attempts a non-blocking handoff of an item to the processor's internal channel for asynchronous processing.
+// Submit attempts a non-blocking handoff of an item to the processor's internal enqueue channel.
 //
-// It returns nil if the item was accepted by the processor, or if the processor is shutting down (in which case the
-// item is immediately finalized with a shutdown error). In both cases, a nil return means the item's lifecycle has been
-// handled by this processor and the caller should not retry.
-// It returns `ErrProcessorBusy` if the processor's channel is momentarily full, signaling that the caller should try
-// another processor.
+// Ownership Contract:
+//   - Returns nil: The item was successfully handed off.
+//     The ShardProcessor takes responsibility for calling Finalize on the item.
+//   - Returns error: The item was not handed off.
+//     Ownership of the FlowItem remains with the caller, who is responsible for calling Finalize.
+//
+// Possible errors:
+//   - ErrProcessorBusy: The processor's input channel is full.
+//   - types.ErrFlowControllerNotRunning: The processor is shutting down.
 func (sp *ShardProcessor) Submit(item *FlowItem) error {
 	if sp.isShuttingDown.Load() {
-		item.Finalize(types.QueueOutcomeRejectedOther,
-			fmt.Errorf("%w: %w", types.ErrRejected, types.ErrFlowControllerNotRunning))
-		return nil // Success from the caller's perspective; the item is terminal.
+		return types.ErrFlowControllerNotRunning
 	}
-
-	select {
+	select { // The default case makes this select non-blocking.
 	case sp.enqueueChan <- item:
-		return nil // Success
+		return nil // Ownership transferred.
+	case <-sp.lifecycleCtx.Done():
+		return types.ErrFlowControllerNotRunning
 	default:
-		// The channel buffer is full, signaling transient backpressure.
 		return ErrProcessorBusy
 	}
 }
 
-// SubmitOrBlock performs a blocking submission of an item to the processor's internal channel.
-// It will wait until either the submission succeeds or the provided context is cancelled.
+// SubmitOrBlock performs a blocking handoff of an item to the processor's internal enqueue channel.
+// It waits until the item is handed off, the caller's context is cancelled, or the processor shuts down.
 //
-// This method is the fallback used by the distributor when all processors are busy, providing graceful backpressure
-// instead of immediate rejection.
+// Ownership Contract:
+//   - Returns nil: The item was successfully handed off.
+//     The ShardProcessor takes responsibility for calling Finalize on the item.
+//   - Returns error: The item was not handed off.
+//     Ownership of the FlowItem remains with the caller, who is responsible for calling Finalize.
 //
-// It returns the `ctx.Err()` if the context is cancelled during the wait.
+// Possible errors:
+//   - ctx.Err(): The provided context was cancelled or its deadline exceeded.
+//   - types.ErrFlowControllerNotRunning: The processor is shutting down.
 func (sp *ShardProcessor) SubmitOrBlock(ctx context.Context, item *FlowItem) error {
 	if sp.isShuttingDown.Load() {
-		// Here, we return an error because the caller, expecting to block, was prevented from doing so by the shutdown.
-		// This is a failure of the operation.
-		item.Finalize(types.QueueOutcomeRejectedOther,
-			fmt.Errorf("%w: %w", types.ErrRejected, types.ErrFlowControllerNotRunning))
 		return types.ErrFlowControllerNotRunning
 	}
 
-	select {
+	select { // The absence of a default case makes this call blocking.
 	case sp.enqueueChan <- item:
-		return nil // Success
+		return nil // Ownership transferred.
 	case <-ctx.Done():
-		// The caller's context was cancelled while we were blocked.
 		return ctx.Err()
+	case <-sp.lifecycleCtx.Done():
+		return types.ErrFlowControllerNotRunning
 	}
 }
 
@@ -155,7 +164,7 @@ func (sp *ShardProcessor) Run(ctx context.Context) {
 	defer sp.logger.V(logutil.DEFAULT).Info("Shard processor run loop stopped.")
 
 	sp.wg.Add(1)
-	go sp.runExpiryCleanup(ctx)
+	go sp.runCleanupSweep(ctx)
 
 	// This is the main worker loop. It continuously processes incoming requests and dispatches queued requests until the
 	// context is cancelled. The `select` statement has three cases:
@@ -204,70 +213,54 @@ func (sp *ShardProcessor) enqueue(item *FlowItem) {
 	req := item.OriginalRequest()
 	key := req.FlowKey()
 
-	logger := log.FromContext(req.Context()).WithName("enqueue").WithValues(
-		"flowKey", key,
-		"flowID", key.ID,
-		"priority", key.Priority,
-		"reqID", req.ID(),
-		"reqByteSize", req.ByteSize(),
-	)
+	// --- Optimistic External Finalization Check ---
+	// Check if the item was finalized by the Controller (due to TTL/cancellation) while it was buffered in enqueueChan.
+	// This is an optimistic check to avoid unnecessary processing on items already considered dead.
+	// The ultimate guarantee of cleanup for any races is the runCleanupSweep mechanism.
+	if finalState := item.FinalState(); finalState != nil {
+		sp.logger.V(logutil.TRACE).Info("Item finalized externally before processing, discarding.",
+			"outcome", finalState.Outcome, "err", finalState.Err, "flowKey", key, "reqID", req.ID())
+		return
+	}
 
+	// --- Configuration Validation ---
 	managedQ, err := sp.shard.ManagedQueue(key)
 	if err != nil {
 		finalErr := fmt.Errorf("configuration error: failed to get queue for flow key %s: %w", key, err)
-		logger.Error(finalErr, "Rejecting item.")
-		item.Finalize(types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, finalErr))
+		sp.logger.Error(finalErr, "Rejecting item.", "flowKey", key, "reqID", req.ID())
+		item.FinalizeWithOutcome(types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, finalErr))
 		return
 	}
 
 	band, err := sp.shard.PriorityBandAccessor(key.Priority)
 	if err != nil {
 		finalErr := fmt.Errorf("configuration error: failed to get priority band for priority %d: %w", key.Priority, err)
-		logger.Error(finalErr, "Rejecting item.")
-		item.Finalize(types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, finalErr))
+		sp.logger.Error(finalErr, "Rejecting item.", "flowKey", key, "reqID", req.ID())
+		item.FinalizeWithOutcome(types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, finalErr))
 		return
 	}
-	logger = logger.WithValues("priorityName", band.PriorityName())
 
+	// --- Capacity Check ---
+	// This check is safe because it is performed by the single-writer Run goroutine.
 	if !sp.hasCapacity(key.Priority, req.ByteSize()) {
-		// This is an expected outcome, not a system error. Log at the default level with rich context.
-		stats := sp.shard.Stats()
-		bandStats := stats.PerPriorityBandStats[key.Priority]
-		logger.V(logutil.DEFAULT).Info("Rejecting request, queue at capacity",
-			"outcome", types.QueueOutcomeRejectedCapacity,
-			"shardTotalBytes", stats.TotalByteSize,
-			"shardCapacityBytes", stats.TotalCapacityBytes,
-			"bandTotalBytes", bandStats.ByteSize,
-			"bandCapacityBytes", bandStats.CapacityBytes,
-		)
-		item.Finalize(types.QueueOutcomeRejectedCapacity, fmt.Errorf("%w: %w", types.ErrRejected, types.ErrQueueAtCapacity))
-		return
-	}
-
-	// This is an optimistic check to prevent a needless add/remove cycle for an item that was finalized (e.g., context
-	// cancelled) during the handoff to this processor. A race condition still exists where an item can be finalized
-	// after this check but before the `Add` call completes.
-	//
-	// This is considered acceptable because:
-	//  1. The race window is extremely small.
-	//  2. The background `runExpiryCleanup` goroutine acts as the ultimate guarantor of correctness, as it will
-	//     eventually find and evict any finalized item that slips through this check and is added to a queue.
-	if item.isFinalized() {
-		finalState := item.finalState
-		outcome, err := finalState.Outcome, finalState.Err
-		logger.V(logutil.VERBOSE).Info("Item finalized before adding to queue, ignoring.", "outcome", outcome, "err", err)
+		sp.logger.V(logutil.DEBUG).Info("Rejecting request, queue at capacity",
+			"flowKey", key, "reqID", req.ID(), "priorityName", band.PriorityName(), "reqByteSize", req.ByteSize())
+		item.FinalizeWithOutcome(types.QueueOutcomeRejectedCapacity, fmt.Errorf("%w: %w",
+			types.ErrRejected, types.ErrQueueAtCapacity))
 		return
 	}
 
-	// This is the point of commitment. After this call, the item is officially in the queue and is the responsibility of
-	// the dispatch or cleanup loops to finalize.
+	// --- Commitment Point ---
+	// The item is admitted. The ManagedQueue.Add implementation is responsible for calling item.SetHandle() atomically.
 	if err := managedQ.Add(item); err != nil {
 		finalErr := fmt.Errorf("failed to add item to queue for flow key %s: %w", key, err)
-		logger.Error(finalErr, "Rejecting item.")
-		item.Finalize(types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, finalErr))
+		sp.logger.Error(finalErr, "Rejecting item post-admission.",
+			"flowKey", key, "reqID", req.ID(), "priorityName", band.PriorityName())
+		item.FinalizeWithOutcome(types.QueueOutcomeRejectedOther, fmt.Errorf("%w: %w", types.ErrRejected, finalErr))
 		return
 	}
-	logger.V(logutil.TRACE).Info("Item enqueued.")
+	sp.logger.V(logutil.TRACE).Info("Item enqueued.",
+		"flowKey", key, "reqID", req.ID(), "priorityName", band.PriorityName())
 }
 
 // hasCapacity checks if the shard and the specific priority band have enough capacity to accommodate an item of a given
@@ -414,145 +407,102 @@ func (sp *ShardProcessor) dispatchItem(itemAcc types.QueueItemAccessor, logger l
 		return fmt.Errorf("failed to remove item %q from queue for flow %s: %w", req.ID(), req.FlowKey(), err)
 	}
 
-	// Final check for expiry/cancellation right before dispatch.
 	removedItem := removedItemAcc.(*FlowItem)
-	isExpired, outcome, expiryErr := checkItemExpiry(removedItem, sp.clock.Now())
-	if isExpired {
-		// Ensure we always have a non-nil error to wrap for consistent logging and error handling.
-		finalErr := expiryErr
-		if finalErr == nil {
-			finalErr = errors.New("item finalized before dispatch")
-		}
-		logger.V(logutil.VERBOSE).Info("Item expired at time of dispatch, evicting", "outcome", outcome,
-			"err", finalErr)
-		removedItem.Finalize(outcome, fmt.Errorf("%w: %w", types.ErrEvicted, finalErr))
-		// Return an error to signal that the dispatch did not succeed.
-		return fmt.Errorf("item %q expired before dispatch: %w", req.ID(), finalErr)
-	}
-
-	// Finalize the item as dispatched.
-	removedItem.Finalize(types.QueueOutcomeDispatched, nil)
-	logger.V(logutil.TRACE).Info("Item dispatched.")
+	sp.logger.V(logutil.TRACE).Info("Item dispatched.", "flowKey", req.FlowKey(), "reqID", req.ID())
+	removedItem.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
 	return nil
 }
 
-// checkItemExpiry provides the authoritative check to determine if an item should be evicted due to TTL expiry or
-// context cancellation.
-//
-// It serves as a safeguard against race conditions. Its first action is to check if the item has already been finalized
-// by a competing goroutine (e.g., the cleanup loop finalizing an item the dispatch loop is trying to process).=
-// This ensures that the final outcome is decided exactly once.
-func checkItemExpiry(
-	itemAcc types.QueueItemAccessor,
-	now time.Time,
-) (isExpired bool, outcome types.QueueOutcome, err error) {
-	item := itemAcc.(*FlowItem)
-
-	// This check is a critical defense against race conditions. If another goroutine (e.g., the cleanup loop) has
-	// already finalized this item, we must respect that outcome.
-	if item.isFinalized() {
-		finalState := item.finalState
-		return true, finalState.Outcome, finalState.Err
-	}
-
-	// Check if the request's context has been cancelled.
-	if ctxErr := item.OriginalRequest().Context().Err(); ctxErr != nil {
-		return true, types.QueueOutcomeEvictedContextCancelled, fmt.Errorf("%w: %w", types.ErrContextCancelled, ctxErr)
-	}
-
-	// Check if the item has outlived its TTL.
-	if item.EffectiveTTL() > 0 && now.Sub(item.EnqueueTime()) > item.EffectiveTTL() {
-		return true, types.QueueOutcomeEvictedTTL, types.ErrTTLExpired
-	}
-
-	return false, types.QueueOutcomeNotYetFinalized, nil
-}
-
-// runExpiryCleanup starts a background goroutine that periodically scans all queues on the shard for expired items.
-func (sp *ShardProcessor) runExpiryCleanup(ctx context.Context) {
+// runCleanupSweep starts a background goroutine that periodically scans all queues for externally finalized items
+// ("zombie" items) and removes them in batches.
+func (sp *ShardProcessor) runCleanupSweep(ctx context.Context) {
 	defer sp.wg.Done()
-	logger := sp.logger.WithName("runExpiryCleanup")
-	logger.V(logutil.DEFAULT).Info("Shard expiry cleanup goroutine starting.")
-	defer logger.V(logutil.DEFAULT).Info("Shard expiry cleanup goroutine stopped.")
+	logger := sp.logger.WithName("runCleanupSweep")
+	logger.V(logutil.DEFAULT).Info("Shard cleanup sweep goroutine starting.")
+	defer logger.V(logutil.DEFAULT).Info("Shard cleanup sweep goroutine stopped.")
 
-	ticker := time.NewTicker(sp.expiryCleanupInterval)
+	ticker := sp.clock.NewTicker(sp.cleanupSweepInterval)
 	defer ticker.Stop()
 
 	for {
 		select {
 		case <-ctx.Done():
 			return
-		case now := <-ticker.C:
-			sp.cleanupExpired(now)
+		case <-ticker.C():
+			sp.sweepFinalizedItems()
 		}
 	}
 }
 
-// cleanupExpired performs a single scan of all queues on the shard, removing and finalizing any items that have
-// expired.
-func (sp *ShardProcessor) cleanupExpired(now time.Time) {
-	processFn := func(managedQ contracts.ManagedQueue, queueLogger logr.Logger) {
-		// This predicate identifies items to be removed by the Cleanup call.
-		predicate := func(item types.QueueItemAccessor) bool {
-			isExpired, _, _ := checkItemExpiry(item, now)
-			return isExpired
+// sweepFinalizedItems performs a single scan of all queues, removing finalized items in batch and releasing their
+// memory.
+func (sp *ShardProcessor) sweepFinalizedItems() {
+	processFn := func(managedQ contracts.ManagedQueue, logger logr.Logger) {
+		key := managedQ.FlowQueueAccessor().FlowKey()
+		predicate := func(itemAcc types.QueueItemAccessor) bool {
+			return itemAcc.(*FlowItem).FinalState() != nil
 		}
-
 		removedItems, err := managedQ.Cleanup(predicate)
 		if err != nil {
-			queueLogger.Error(err, "Error during ManagedQueue Cleanup")
+			logger.Error(err, "Error during ManagedQueue Cleanup", "flowKey", key)
 		}
-
-		// Finalize all the items that were removed.
-		sp.finalizeExpiredItems(removedItems, now, queueLogger)
+		logger.V(logutil.DEBUG).Info("Swept finalized items and released capacity.",
+			"flowKey", key, "count", len(removedItems))
 	}
-	sp.processAllQueuesConcurrently("cleanupExpired", processFn)
+	sp.processAllQueuesConcurrently("sweepFinalizedItems", processFn)
 }
 
-// shutdown handles the graceful termination of the processor, ensuring any pending items in the enqueue channel or in
-// the queues are finalized correctly.
+// shutdown handles the graceful termination of the processor, ensuring all pending items (in channel and queues) are
+// Finalized.
 func (sp *ShardProcessor) shutdown() {
 	sp.shutdownOnce.Do(func() {
-		// Set the atomic bool so that any new calls to Enqueue will fail fast.
 		sp.isShuttingDown.Store(true)
 		sp.logger.V(logutil.DEFAULT).Info("Shard processor shutting down.")
 
-		// Drain the channel BEFORE closing it. This prevents a panic from any goroutine that is currently blocked trying to
-		// send to the channel. We read until it's empty.
-	DrainLoop:
+	DrainLoop: // Drain the enqueueChan to finalize buffered items.
 		for {
 			select {
 			case item := <-sp.enqueueChan:
-				if item == nil { // This is a safeguard against logic errors in the distributor.
+				if item == nil {
 					continue
 				}
-				item.Finalize(types.QueueOutcomeRejectedOther,
+				// Finalize buffered items.
+				item.FinalizeWithOutcome(types.QueueOutcomeRejectedOther,
 					fmt.Errorf("%w: %w", types.ErrRejected, types.ErrFlowControllerNotRunning))
 			default:
-				// The channel is empty, we can now safely close it.
 				break DrainLoop
 			}
 		}
-		close(sp.enqueueChan)
-
-		// Evict all remaining items from the queues.
+		// We do not close enqueueChan because external goroutines (Controller) send on it.
+		// The channel will be garbage collected when the processor terminates.
 		sp.evictAll()
 	})
 }
 
-// evictAll drains all queues on the shard and finalizes every item with a shutdown error.
+// evictAll drains all queues on the shard, finalizes every item, and releases their memory.
 func (sp *ShardProcessor) evictAll() {
-	processFn := func(managedQ contracts.ManagedQueue, queueLogger logr.Logger) {
+	processFn := func(managedQ contracts.ManagedQueue, logger logr.Logger) {
+		key := managedQ.FlowQueueAccessor().FlowKey()
 		removedItems, err := managedQ.Drain()
 		if err != nil {
-			queueLogger.Error(err, "Error during ManagedQueue Drain")
+			logger.Error(err, "Error during ManagedQueue Drain", "flowKey", key)
 		}
 
-		// Finalize all the items that were removed.
-		getOutcome := func(_ types.QueueItemAccessor) (types.QueueOutcome, error) {
-			return types.QueueOutcomeEvictedOther, fmt.Errorf("%w: %w", types.ErrEvicted, types.ErrFlowControllerNotRunning)
+		outcome := types.QueueOutcomeEvictedOther
+		errShutdown := fmt.Errorf("%w: %w", types.ErrEvicted, types.ErrFlowControllerNotRunning)
+		for _, i := range removedItems {
+			item, ok := i.(*FlowItem)
+			if !ok {
+				logger.Error(fmt.Errorf("internal error: unexpected type %T", i),
+					"Panic condition detected during shutdown", "flowKey", key)
+				continue
+			}
+
+			// Finalization is idempotent; safe to call even if already finalized externally.
+			item.FinalizeWithOutcome(outcome, errShutdown)
+			logger.V(logutil.TRACE).Info("Item evicted during shutdown.",
+				"flowKey", key, "reqID", item.OriginalRequest().ID())
 		}
-		sp.finalizeItems(removedItems, queueLogger, getOutcome)
 	}
 	sp.processAllQueuesConcurrently("evictAll", processFn)
 }
@@ -620,38 +570,3 @@ func (sp *ShardProcessor) processAllQueuesConcurrently(
 	close(tasks) // Close the channel to signal workers to exit.
 	wg.Wait()    // Wait for all workers to finish.
 }
-
-// finalizeItems is a helper to iterate over a slice of items, safely cast them, and finalize them with an outcome
-// determined by the `getOutcome` function.
-func (sp *ShardProcessor) finalizeItems(
-	items []types.QueueItemAccessor,
-	logger logr.Logger,
-	getOutcome func(item types.QueueItemAccessor) (types.QueueOutcome, error),
-) {
-	for _, i := range items {
-		item, ok := i.(*FlowItem)
-		if !ok {
-			unexpectedItemErr := fmt.Errorf("internal error: item %q of type %T is not a *FlowItem",
-				i.OriginalRequest().ID(), i)
-			logger.Error(unexpectedItemErr, "Panic condition detected during finalization", "item", i)
-			continue
-		}
-
-		outcome, err := getOutcome(i)
-		item.Finalize(outcome, err)
-		logger.V(logutil.TRACE).Info("Item finalized", "reqID", item.OriginalRequest().ID(),
-			"outcome", outcome, "err", err)
-	}
-}
-
-// finalizeExpiredItems is a specialized version of finalizeItems for items that are known to be expired.
-// It determines the precise reason for expiry and finalizes the item accordingly.
-func (sp *ShardProcessor) finalizeExpiredItems(items []types.QueueItemAccessor, now time.Time, logger logr.Logger) {
-	getOutcome := func(item types.QueueItemAccessor) (types.QueueOutcome, error) {
-		// We don't need the `isExpired` boolean here because we know it's true, but this function conveniently returns the
-		// precise outcome and error.
-		_, outcome, expiryErr := checkItemExpiry(item, now)
-		return outcome, fmt.Errorf("%w: %w", types.ErrEvicted, expiryErr)
-	}
-	sp.finalizeItems(items, logger, getOutcome)
-}
diff --git a/pkg/epp/flowcontrol/types/mocks/mocks.go b/pkg/epp/flowcontrol/types/mocks/mocks.go
index c52c5c2..5fabf36 100644
--- a/pkg/epp/flowcontrol/types/mocks/mocks.go
+++ b/pkg/epp/flowcontrol/types/mocks/mocks.go
@@ -19,7 +19,6 @@ limitations under the License.
 package mocks
 
 import (
-	"context"
 	"time"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
@@ -28,7 +27,6 @@ import (
 
 // MockFlowControlRequest provides a mock implementation of the `types.FlowControlRequest` interface.
 type MockFlowControlRequest struct {
-	Ctx                         context.Context
 	FlowKeyV                    types.FlowKey
 	ByteSizeV                   uint64
 	InitialEffectiveTTLV        time.Duration
@@ -41,20 +39,14 @@ func NewMockFlowControlRequest(
 	byteSize uint64,
 	id string,
 	key types.FlowKey,
-	ctx context.Context,
 ) *MockFlowControlRequest {
-	if ctx == nil {
-		ctx = context.Background()
-	}
 	return &MockFlowControlRequest{
 		ByteSizeV: byteSize,
 		IDV:       id,
 		FlowKeyV:  key,
-		Ctx:       ctx,
 	}
 }
 
-func (m *MockFlowControlRequest) Context() context.Context           { return m.Ctx }
 func (m *MockFlowControlRequest) FlowKey() types.FlowKey             { return m.FlowKeyV }
 func (m *MockFlowControlRequest) ByteSize() uint64                   { return m.ByteSizeV }
 func (m *MockFlowControlRequest) InitialEffectiveTTL() time.Duration { return m.InitialEffectiveTTLV }
@@ -114,7 +106,6 @@ func NewMockQueueItemAccessor(byteSize uint64, reqID string, key types.FlowKey)
 			byteSize,
 			reqID,
 			key,
-			context.Background(),
 		),
 		HandleV: &MockQueueItemHandle{},
 	}
diff --git a/pkg/epp/flowcontrol/types/request.go b/pkg/epp/flowcontrol/types/request.go
index 255d3bc..e427b0a 100644
--- a/pkg/epp/flowcontrol/types/request.go
+++ b/pkg/epp/flowcontrol/types/request.go
@@ -17,7 +17,6 @@ limitations under the License.
 package types
 
 import (
-	"context"
 	"time"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
@@ -30,11 +29,6 @@ import (
 // wraps this object with its own internal structures (which implement `QueueItemAccessor`) to manage the request's
 // lifecycle without modifying the original.
 type FlowControlRequest interface {
-	// Context returns the request's context. The `controller.FlowController` uses this for monitoring cancellation (e.g.,
-	// if the client disconnects or a request-scoped timeout occurs), which can lead to the request being evicted from a
-	// queue.
-	Context() context.Context
-
 	// FlowKey returns the composite key that uniquely identifies the flow instance this request belongs to.
 	// The `controller.FlowController` uses this key as the primary identifier to look up the correct
 	// `contracts.ManagedQueue` and configured `framework.IntraFlowDispatchPolicy` from a `contracts.RegistryShard`.
diff --git a/pkg/epp/handlers/request.go b/pkg/epp/handlers/request.go
index 7f81221..0e04289 100644
--- a/pkg/epp/handlers/request.go
+++ b/pkg/epp/handlers/request.go
@@ -29,6 +29,13 @@ import (
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
 )
 
+const (
+	// defaultFairnessID is the default fairness ID used when no ID is provided in the request.
+	// This ensures that requests without explicit fairness identifiers are still grouped and managed by the Flow Control
+	// system.
+	defaultFairnessID = "default-flow"
+)
+
 func (s *StreamingServer) HandleRequestHeaders(reqCtx *RequestContext, req *extProcPb.ProcessingRequest_RequestHeaders) error {
 	reqCtx.RequestReceivedTimestamp = time.Now()
 
@@ -80,6 +87,11 @@ func (s *StreamingServer) HandleRequestHeaders(reqCtx *RequestContext, req *extP
 			delete(reqCtx.Request.Headers, header.Key)
 		}
 	}
+
+	if reqCtx.FairnessID == "" {
+		reqCtx.FairnessID = defaultFairnessID
+	}
+
 	return nil
 }
 
diff --git a/pkg/epp/metrics/metrics.go b/pkg/epp/metrics/metrics.go
index cefc4d4..db361a4 100644
--- a/pkg/epp/metrics/metrics.go
+++ b/pkg/epp/metrics/metrics.go
@@ -235,6 +235,28 @@ var (
 		},
 		[]string{"commit", "build_ref"},
 	)
+
+	// Flow Control Metrics
+	flowControlRequestQueueDuration = prometheus.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Subsystem: InferenceExtension,
+			Name:      "flow_control_request_queue_duration_seconds",
+			Help:      metricsutil.HelpMsgWithStability("Distribution of the total time requests spend in the EPP flow control layer, measured from the start of the EnqueueAndWait call until a final outcome is reached.", compbasemetrics.ALPHA),
+			Buckets: []float64{
+				0.0001, 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0,
+			},
+		},
+		[]string{"fairness_id", "priority", "outcome"},
+	)
+
+	flowControlQueueSize = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Subsystem: InferenceExtension,
+			Name:      "flow_control_queue_size",
+			Help:      metricsutil.HelpMsgWithStability("Current number of requests being actively managed by the EPP flow control layer, from the start of the EnqueueAndWait call until a final outcome is reached.", compbasemetrics.ALPHA),
+		},
+		[]string{"fairness_id", "priority"},
+	)
 )
 
 var registerMetrics sync.Once
@@ -260,6 +282,8 @@ func Register(customCollectors ...prometheus.Collector) {
 		metrics.Registry.MustRegister(PrefixCacheSize)
 		metrics.Registry.MustRegister(PrefixCacheHitRatio)
 		metrics.Registry.MustRegister(PrefixCacheHitLength)
+		metrics.Registry.MustRegister(flowControlRequestQueueDuration)
+		metrics.Registry.MustRegister(flowControlQueueSize)
 		for _, collector := range customCollectors {
 			metrics.Registry.MustRegister(collector)
 		}
@@ -286,6 +310,8 @@ func Reset() {
 	PrefixCacheSize.Reset()
 	PrefixCacheHitRatio.Reset()
 	PrefixCacheHitLength.Reset()
+	flowControlRequestQueueDuration.Reset()
+	flowControlQueueSize.Reset()
 }
 
 // RecordRequstCounter records the number of requests.
@@ -414,3 +440,18 @@ func RecordPrefixCacheMatch(matchedLength, totalLength int) {
 func RecordInferenceExtensionInfo(commitSha, buildRef string) {
 	InferenceExtensionInfo.WithLabelValues(commitSha, buildRef).Set(1)
 }
+
+// RecordFlowControlRequestQueueDuration records the duration a request spent in the Flow Control layer.
+func RecordFlowControlRequestQueueDuration(fairnessID, priority, outcome string, duration time.Duration) {
+	flowControlRequestQueueDuration.WithLabelValues(fairnessID, priority, outcome).Observe(duration.Seconds())
+}
+
+// IncFlowControlQueueSize increments the Flow Control queue size gauge.
+func IncFlowControlQueueSize(fairnessID, priority string) {
+	flowControlQueueSize.WithLabelValues(fairnessID, priority).Inc()
+}
+
+// DecFlowControlQueueSize decrements the Flow Control queue size gauge.
+func DecFlowControlQueueSize(fairnessID, priority string) {
+	flowControlQueueSize.WithLabelValues(fairnessID, priority).Dec()
+}
diff --git a/pkg/epp/plugins/handle.go b/pkg/epp/plugins/handle.go
index 8c9153c..c074e90 100644
--- a/pkg/epp/plugins/handle.go
+++ b/pkg/epp/plugins/handle.go
@@ -19,6 +19,8 @@ package plugins
 import (
 	"context"
 	"fmt"
+
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 )
 
 // Handle provides plugins a set of standard data and tools to work with
@@ -27,6 +29,9 @@ type Handle interface {
 	Context() context.Context
 
 	HandlePlugins
+
+	// PodList lists pods matching the given predicate.
+	PodList(predicate func(backendmetrics.PodMetrics) bool) []backendmetrics.PodMetrics
 }
 
 // HandlePlugins defines a set of APIs to work with instantiated plugins
@@ -44,10 +49,14 @@ type HandlePlugins interface {
 	GetAllPluginsWithNames() map[string]Plugin
 }
 
+// PodListFunc is a function type that filters and returns a list of pod metrics
+type PodListFunc func(predicate func(backendmetrics.PodMetrics) bool) []backendmetrics.PodMetrics
+
 // eppHandle is an implementation of the interface plugins.Handle
 type eppHandle struct {
 	ctx context.Context
 	HandlePlugins
+	podList PodListFunc
 }
 
 // Context returns a context the plugins can use, if they need one
@@ -84,12 +93,18 @@ func (h *eppHandlePlugins) GetAllPluginsWithNames() map[string]Plugin {
 	return h.plugins
 }
 
-func NewEppHandle(ctx context.Context) Handle {
+// PodList lists pods matching the given predicate.
+func (h *eppHandle) PodList(predicate func(backendmetrics.PodMetrics) bool) []backendmetrics.PodMetrics {
+	return h.podList(predicate)
+}
+
+func NewEppHandle(ctx context.Context, podList PodListFunc) Handle {
 	return &eppHandle{
 		ctx: ctx,
 		HandlePlugins: &eppHandlePlugins{
 			plugins: map[string]Plugin{},
 		},
+		podList: podList,
 	}
 }
 
diff --git a/pkg/epp/requestcontrol/admission.go b/pkg/epp/requestcontrol/admission.go
new file mode 100644
index 0000000..69fd5ad
--- /dev/null
+++ b/pkg/epp/requestcontrol/admission.go
@@ -0,0 +1,216 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package requestcontrol
+
+import (
+	"context"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
+	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"
+)
+
+// AdmissionController defines the interface for making admission control decisions.
+// Implementations of this interface determine whether an incoming inference request should be accepted or rejected
+// based on various criteria such as system load, fairness, priority, and available capacity.
+type AdmissionController interface {
+	// Admit determines if a request should be admitted.
+	// It is called by the Director for each incoming request.
+	//
+	// Args:
+	//   ctx: The request context, carrying deadlines, cancellation signals, and logger.
+	//   reqCtx: The handlers.RequestContext containing details about the incoming request.
+	//   candidatePods: A list of potential backend pods that can serve the request.
+	//   priority: The priority level of the request, as determined by the InferenceObjective.
+	//
+	// Returns:
+	//   - nil: If the request is admitted and should proceed to scheduling.
+	//   - errutil.Error: If the request is rejected.
+	Admit(
+		ctx context.Context,
+		reqCtx *handlers.RequestContext,
+		candidatePods []backendmetrics.PodMetrics,
+		priority int,
+	) error
+}
+
+// saturationDetector defines the minimal interface required for checking if the backend pool is saturated.
+type saturationDetector interface {
+	IsSaturated(ctx context.Context, candidatePods []backendmetrics.PodMetrics) bool
+}
+
+// flowController defines the minimal interface required by FlowControlAdmissionController for enqueuing requests and
+// waiting for an admission outcome.
+type flowController interface {
+	EnqueueAndWait(ctx context.Context, req types.FlowControlRequest) (types.QueueOutcome, error)
+}
+
+// rejectIfSheddableAndSaturated checks if a request should be immediately rejected because it's sheddable
+// (priority < 0) and the system is saturated.
+func rejectIfSheddableAndSaturated(
+	ctx context.Context,
+	sd saturationDetector,
+	reqCtx *handlers.RequestContext,
+	candidatePods []backendmetrics.PodMetrics,
+	priority int,
+) error {
+	if requtil.IsSheddable(priority) {
+		logger := log.FromContext(ctx)
+		if sd.IsSaturated(ctx, candidatePods) {
+			logger.V(logutil.TRACE).Info("Request rejected: system saturated and request is sheddable",
+				"requestID", reqCtx.SchedulingRequest.RequestId)
+			return errutil.Error{
+				Code: errutil.InferencePoolResourceExhausted,
+				Msg:  "system saturated, sheddable request dropped",
+			}
+		}
+	}
+	return nil
+}
+
+// --- LegacyAdmissionController ---
+
+// LegacyAdmissionController implements saturation-based admission control.
+// It rejects sheddable requests (priority < 0) if the saturationDetector indicates that the system is currently
+// saturated. Non-sheddable requests always bypass the saturation check.
+type LegacyAdmissionController struct {
+	saturationDetector saturationDetector
+}
+
+// NewLegacyAdmissionController creates a new LegacyAdmissionController.
+func NewLegacyAdmissionController(sd saturationDetector) *LegacyAdmissionController {
+	return &LegacyAdmissionController{saturationDetector: sd}
+}
+
+// Admit implements the AdmissionController interface for the legacy strategy.
+// It checks for saturation only for requests with priority < 0.
+func (lac *LegacyAdmissionController) Admit(
+	ctx context.Context,
+	reqCtx *handlers.RequestContext,
+	candidatePods []backendmetrics.PodMetrics,
+	priority int,
+) error {
+	logger := log.FromContext(ctx)
+	logger.V(logutil.TRACE).Info("Executing LegacyAdmissionController",
+		"priority", priority, "fairnessID", reqCtx.FairnessID)
+	if err := rejectIfSheddableAndSaturated(ctx, lac.saturationDetector, reqCtx, candidatePods, priority); err != nil {
+		return err
+	}
+	logger.V(logutil.TRACE).Info("Request admitted", "requestID", reqCtx.SchedulingRequest.RequestId)
+	return nil
+}
+
+// --- FlowControlAdmissionController ---
+
+// FlowControlAdmissionController delegates admission decisions to the Flow Control layer.
+// It first checks if the request is sheddable and the system is saturated, rejecting immediately if both conditions are
+// true. Otherwise, it uses the provided flowController to enqueue the request and await an outcome.
+type FlowControlAdmissionController struct {
+	saturationDetector saturationDetector
+	flowController     flowController
+}
+
+// NewFlowControlAdmissionController creates a new FlowControlAdmissionController.
+// It requires a SaturationDetector and a flowController instance.
+func NewFlowControlAdmissionController(sd saturationDetector, fc flowController) *FlowControlAdmissionController {
+	return &FlowControlAdmissionController{
+		saturationDetector: sd,
+		flowController:     fc,
+	}
+}
+
+// Admit implements the AdmissionController interface by checking for saturation on sheddable requests first, then
+// deferring to the Flow Control system.
+func (fcac *FlowControlAdmissionController) Admit(
+	ctx context.Context,
+	reqCtx *handlers.RequestContext,
+	candidatePods []backendmetrics.PodMetrics,
+	priority int,
+) error {
+	logger := log.FromContext(ctx)
+	logger.V(logutil.TRACE).Info("Executing FlowControlAdmissionController",
+		"requestID", reqCtx.SchedulingRequest.RequestId, "priority", priority, "fairnessID", reqCtx.FairnessID)
+	if err := rejectIfSheddableAndSaturated(ctx, fcac.saturationDetector, reqCtx, candidatePods, priority); err != nil {
+		return err
+	}
+
+	logger.V(logutil.TRACE).Info("Request proceeding to flow control", "requestID", reqCtx.SchedulingRequest.RequestId)
+
+	fcReq := &flowControlRequest{
+		requestID:       reqCtx.SchedulingRequest.RequestId,
+		fairnessID:      reqCtx.FairnessID,
+		priority:        priority,
+		requestByteSize: uint64(reqCtx.RequestSize),
+		candidatePods:   candidatePods,
+	}
+
+	outcome, err := fcac.flowController.EnqueueAndWait(ctx, fcReq)
+	logger.V(logutil.DEBUG).Info("Flow control outcome",
+		"requestID", reqCtx.SchedulingRequest.RequestId, "outcome", outcome, "error", err)
+	return translateFlowControlOutcome(outcome, err)
+}
+
+// flowControlRequest is an adapter that implements the types.FlowControlRequest interface.
+type flowControlRequest struct {
+	requestID       string
+	fairnessID      string
+	priority        int
+	requestByteSize uint64
+	candidatePods   []backendmetrics.PodMetrics
+}
+
+var _ types.FlowControlRequest = &flowControlRequest{}
+
+func (r *flowControlRequest) ID() string                         { return r.requestID }
+func (r *flowControlRequest) InitialEffectiveTTL() time.Duration { return 0 } // Use controller default.
+func (r *flowControlRequest) ByteSize() uint64                   { return r.requestByteSize }
+func (r *flowControlRequest) CandidatePodsForScheduling() []backendmetrics.PodMetrics {
+	return r.candidatePods
+}
+func (r *flowControlRequest) FlowKey() types.FlowKey {
+	return types.FlowKey{ID: r.fairnessID, Priority: r.priority}
+}
+
+// translateFlowControlOutcome maps the context-rich outcome of the Flow Control layer to the public errutil.Error
+// contract used by the Director.
+func translateFlowControlOutcome(outcome types.QueueOutcome, err error) error {
+	msg := "request rejected by flow control"
+	if err != nil {
+		msg = err.Error()
+	}
+
+	switch outcome {
+	case types.QueueOutcomeDispatched:
+		return nil
+	case types.QueueOutcomeRejectedCapacity:
+		return errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: msg}
+	case types.QueueOutcomeEvictedTTL:
+		return errutil.Error{Code: errutil.ServiceUnavailable, Msg: "request timed out in queue: " + msg}
+	case types.QueueOutcomeEvictedContextCancelled:
+		return errutil.Error{Code: errutil.ServiceUnavailable, Msg: "client disconnected: " + msg}
+	case types.QueueOutcomeRejectedOther, types.QueueOutcomeEvictedOther:
+		return errutil.Error{Code: errutil.Internal, Msg: "internal flow control error: " + msg}
+	default:
+		return errutil.Error{Code: errutil.Internal, Msg: "unhandled flow control outcome: " + msg}
+	}
+}
diff --git a/pkg/epp/requestcontrol/director.go b/pkg/epp/requestcontrol/director.go
index a3e2d6d..2f8c738 100644
--- a/pkg/epp/requestcontrol/director.go
+++ b/pkg/epp/requestcontrol/director.go
@@ -54,28 +54,36 @@ type Scheduler interface {
 	Schedule(ctx context.Context, request *schedulingtypes.LLMRequest, candidatePods []schedulingtypes.Pod) (result *schedulingtypes.SchedulingResult, err error)
 }
 
-// SaturationDetector provides a signal indicating whether the backends are considered saturated.
-type SaturationDetector interface {
-	IsSaturated(ctx context.Context, candidatePods []backendmetrics.PodMetrics) bool
-}
-
 // NewDirectorWithConfig creates a new Director instance with all dependencies.
-func NewDirectorWithConfig(datastore Datastore, scheduler Scheduler, saturationDetector SaturationDetector, config *Config) *Director {
+func NewDirectorWithConfig(
+	datastore Datastore,
+	scheduler Scheduler,
+	admissionController AdmissionController,
+	config *Config,
+) *Director {
 	return &Director{
 		datastore:           datastore,
 		scheduler:           scheduler,
-		saturationDetector:  saturationDetector,
+		admissionController: admissionController,
 		preRequestPlugins:   config.preRequestPlugins,
 		postResponsePlugins: config.postResponsePlugins,
 		defaultPriority:     0, // define default priority explicitly
 	}
 }
 
-// Director orchestrates the request handling flow, including scheduling.
+// Director orchestrates the request handling flow after initial parsing by the handler.
+// Its responsibilities include:
+// - Retrieving request metadata and relevant objectives.
+// - Determining candidate pods.
+// - Performing admission control via the AdmissionController.
+// - Scheduling the request to target pod(s) via the Scheduler.
+// - Running PreRequest plugins.
+// - Preparing the request context for the Envoy ext_proc filter to route the request.
+// - Running PostResponse plugins.
 type Director struct {
 	datastore           Datastore
 	scheduler           Scheduler
-	saturationDetector  SaturationDetector
+	admissionController AdmissionController
 	preRequestPlugins   []PreRequest
 	postResponsePlugins []PostResponse
 	// we just need a pointer to an int variable since priority is a pointer in InferenceObjective
@@ -140,8 +148,8 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 		return reqCtx, errutil.Error{Code: errutil.ServiceUnavailable, Msg: "failed to find candidate pods for serving the request"}
 	}
 
-	// Admission Control check
-	if err := d.admitRequest(ctx, candidatePods, *infObjective.Spec.Priority, reqCtx.FairnessID); err != nil {
+	if err := d.admissionController.Admit(ctx, reqCtx, candidatePods, *infObjective.Spec.Priority); err != nil {
+		logger.V(logutil.DEFAULT).Info("Request rejected by admission control", "error", err)
 		return reqCtx, err
 	}
 
@@ -207,31 +215,6 @@ func (d *Director) getCandidatePodsForScheduling(ctx context.Context, requestMet
 	return podFilteredList
 }
 
-// admitRequest handles admission control to decide whether or not to accept the request
-// based on the request priority and saturation state.
-func (d *Director) admitRequest(ctx context.Context, candidatePods []backendmetrics.PodMetrics, requestPriority int, fairnessID string) error {
-	loggerTrace := log.FromContext(ctx).V(logutil.TRACE)
-
-	loggerTrace.Info("Entering Flow Control", "priority", requestPriority, "fairnessID", fairnessID)
-
-	// This will be removed in favor of a more robust implementation (Flow Control) in the very near future.
-	// TODO: Make this a configurable value.
-	// Tracking issue https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/1347
-	if requestPriority >= 0 {
-		loggerTrace.Info("Non-sheddable request bypassing saturation check.")
-		return nil
-	}
-
-	if d.saturationDetector.IsSaturated(ctx, candidatePods) {
-		return errutil.Error{
-			Code: errutil.InferencePoolResourceExhausted,
-			Msg:  "system saturated, sheddable request dropped",
-		}
-	}
-
-	return nil
-}
-
 // prepareRequest populates the RequestContext and calls the registered PreRequest plugins
 // for allowing plugging customized logic based on the scheduling result.
 func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestContext, result *schedulingtypes.SchedulingResult) (*handlers.RequestContext, error) {
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer.go
index bd9e2c9..8b68132 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer.go
@@ -149,3 +149,35 @@ func (i *indexer) reportLRUSize(ctx context.Context, interval time.Duration) {
 		i.mu.RUnlock()
 	}
 }
+
+// RemovePod removes a pod and its associated entries from the indexer.
+func (i *indexer) RemovePod(pod ServerID) {
+	i.mu.RLock()
+	lruCache, exists := i.podToLRU[pod]
+	i.mu.RUnlock()
+
+	if !exists {
+		return
+	}
+
+	// Remove all hashes associated with the pod from hashToPods (triggers eviction callbacks).
+	for _, hash := range lruCache.Keys() {
+		lruCache.Remove(hash)
+	}
+
+	i.mu.Lock()
+	delete(i.podToLRU, pod)
+	i.mu.Unlock()
+}
+
+// Pods returns the list of all pods currently tracked in the indexer.
+func (i *indexer) Pods() []ServerID {
+	i.mu.RLock()
+	defer i.mu.RUnlock()
+
+	pods := make([]ServerID, 0, len(i.podToLRU))
+	for pod := range i.podToLRU {
+		pods = append(pods, pod)
+	}
+	return pods
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
index 6bc81a4..16af272 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
@@ -22,11 +22,13 @@ import (
 	"encoding/json"
 	"fmt"
 	"sync"
+	"time"
 
 	"github.com/cespare/xxhash/v2"
 	k8stypes "k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
@@ -57,6 +59,10 @@ const (
 	PrefixCachePluginType = "prefix-cache-scorer"
 )
 
+const (
+	PodActiveCheckInterval = 2 * time.Minute
+)
+
 var DefaultConfig = Config{
 	BlockSize:              DefaultBlockSize,
 	MaxPrefixBlocksToMatch: DefaultMaxPrefixBlocks,
@@ -88,6 +94,8 @@ type podSet map[ServerID]struct{}
 type Indexer interface {
 	Get(hash BlockHash) podSet
 	Add(hashes []BlockHash, server ServerID)
+	RemovePod(server ServerID)
+	Pods() []ServerID
 }
 
 // BlockHash is a hash of the block of request body.
@@ -144,7 +152,9 @@ func PrefixCachePluginFactory(name string, rawParameters json.RawMessage, handle
 		}
 	}
 
-	return New(handle.Context(), parameters).WithName(name), nil
+	p := New(handle.Context(), parameters).WithName(name)
+	go p.CleanUpInactivePods(handle.Context(), handle)
+	return p, nil
 }
 
 // New initializes a new prefix Plugin and returns its pointer.
@@ -256,8 +266,35 @@ func (p *Plugin) matchLongestPrefix(ctx context.Context, hashes []BlockHash) map
 	return res
 }
 
+// CleanUpInactivePods starts a goroutine that watches for inactive pods.
+func (m *Plugin) CleanUpInactivePods(ctx context.Context, handle plugins.Handle) {
+	logger := log.FromContext(ctx).V(logutil.VERBOSE)
+	ticker := time.NewTicker(PodActiveCheckInterval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-ticker.C:
+			activePodMetrics := handle.PodList(func(_ backendmetrics.PodMetrics) bool { return true })
+			activePods := make(map[ServerID]struct{}, len(activePodMetrics))
+			for _, pm := range activePodMetrics {
+				activePods[ServerID(pm.GetPod().NamespacedName)] = struct{}{}
+			}
+
+			for _, pod := range m.indexer.Pods() {
+				if _, ok := activePods[pod]; !ok {
+					m.indexer.RemovePod(pod)
+					logger.Info("Removed pod not in active set", "pod", pod)
+				}
+			}
+		}
+	}
+}
+
 // hashPrompt divides the prompt into blocks and calculate the prefix cache for each block.
-// hash(0) is the hash of the model name, since different models generally don't share prefix cache.
+// hash[0] is calculated including the model name and cache_salt(if provided), since different models generally don't share prefix cache.
 // For block i, hash(i) = hash(block i content, hash(i-1)).
 func hashPrompt(ctx context.Context, request *types.LLMRequest, cacheBlockSize int, maxPrefixBlocks int) []BlockHash {
 	loggerDebug := log.FromContext(ctx).V(logutil.DEBUG)
@@ -286,6 +323,10 @@ func hashPrompt(ctx context.Context, request *types.LLMRequest, cacheBlockSize i
 	// Add the model to the first block hash so that different models have different hashes even with the same body.
 	h := xxhash.New()
 	_, _ = h.Write([]byte(request.TargetModel))
+	if cacheSalt := request.Body.CacheSalt(); cacheSalt != "" {
+		_, _ = h.Write([]byte(cacheSalt))
+	}
+
 	prevBlockHash := BlockHash(h.Sum64())
 	for i := 0; i+cacheBlockSize <= len(userInput); i += cacheBlockSize {
 		h.Reset()
diff --git a/pkg/epp/scheduling/types/types.go b/pkg/epp/scheduling/types/types.go
index 2685a22..c93f0c5 100644
--- a/pkg/epp/scheduling/types/types.go
+++ b/pkg/epp/scheduling/types/types.go
@@ -56,6 +56,18 @@ type LLMRequestBody struct {
 	ChatCompletions *ChatCompletionsRequest `json:"chat_completions,omitempty"`
 }
 
+func (r *LLMRequestBody) CacheSalt() string {
+	if r.ChatCompletions == nil && r.Completions == nil {
+		return ""
+	}
+
+	if r.ChatCompletions != nil {
+		return r.ChatCompletions.CacheSalt
+	}
+
+	return r.Completions.CacheSalt
+}
+
 // CompletionsRequest is a structured representation of the fields we parse out of the
 // /v1/completions request body.
 // This struct includes fields usable for plugins and scheduling decisions - and not the entire
@@ -63,6 +75,8 @@ type LLMRequestBody struct {
 type CompletionsRequest struct {
 	// Prompt is the prompt that was sent in the request body.
 	Prompt string `json:"prompt,omitempty"`
+	// CacheSalt is an optional request parameter to isolate prefix caches for security reasons.
+	CacheSalt string `json:"cache_salt,omitempty"`
 }
 
 func (r *CompletionsRequest) String() string {
@@ -88,6 +102,8 @@ type ChatCompletionsRequest struct {
 	ContinueFinalMessage      bool                   `json:"continue_final_message,omitempty"`
 	AddGenerationPrompt       bool                   `json:"add_generation_prompt,omitempty"`
 	ChatTemplateKWArgs        map[string]interface{} `json:"chat_template_kwargs,omitempty"`
+	// CacheSalt is an optional request parameter to isolate prefix caches for security reasons.
+	CacheSalt string `json:"cache_salt,omitempty"`
 }
 
 func (r *ChatCompletionsRequest) String() string {
diff --git a/pkg/epp/server/runserver.go b/pkg/epp/server/runserver.go
index 870acc7..1d9d1d1 100644
--- a/pkg/epp/server/runserver.go
+++ b/pkg/epp/server/runserver.go
@@ -42,6 +42,7 @@ import (
 	"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/requestcontrol"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/saturationdetector"
 )
 
 // ExtProcServerRunner provides methods to manage an external process server.
@@ -56,7 +57,7 @@ type ExtProcServerRunner struct {
 	RefreshPrometheusMetricsInterval time.Duration
 	MetricsStalenessThreshold        time.Duration
 	Director                         *requestcontrol.Director
-	SaturationDetector               requestcontrol.SaturationDetector
+	SaturationDetector               *saturationdetector.Detector
 	UseExperimentalDatalayerV2       bool // Pluggable data layer feature flag
 
 	// This should only be used in tests. We won't need this once we do not inject metrics in the tests.
diff --git a/pkg/epp/util/request/sheddable.go b/pkg/epp/util/request/sheddable.go
new file mode 100644
index 0000000..c2f32c1
--- /dev/null
+++ b/pkg/epp/util/request/sheddable.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 request
+
+// IsSheddable determines if a request is considered sheddable based on its priority.
+func IsSheddable(priority int) bool {
+	return priority < 0
+}
diff --git a/pkg/generator/main.go b/pkg/generator/main.go
index 3cb70a5..c663588 100644
--- a/pkg/generator/main.go
+++ b/pkg/generator/main.go
@@ -35,6 +35,7 @@ import (
 func main() {
 	roots, err := loader.LoadRoots(
 		"k8s.io/apimachinery/pkg/runtime/schema", // Needed to parse generated register functions.
+		"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha1",
 		"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2",
 		"sigs.k8s.io/gateway-api-inference-extension/api/v1",
 	)
diff --git a/site-src/api-types/inferenceobjective.md b/site-src/api-types/inferenceobjective.md
index 9b7fe74..8c48651 100644
--- a/site-src/api-types/inferenceobjective.md
+++ b/site-src/api-types/inferenceobjective.md
@@ -9,6 +9,10 @@
 
 The **InferenceObjective** API defines a set of serving objectives of the specific request it is associated with. This CRD currently houses only `Priority` but will be expanded to include fields such as SLO attainment.
 
+## Usage
+
+To associate a request to the InferencePool with a specific InferenceObjective, the system uses a specific header: `x-gateway-inference-objective` with the value of the header set to the InferenceObjective metadata name. So the calling client must set the header key/value on the request to associate the selected InferenceObjective. If no InferenceObjective is selected, default values are used.  
+
 ## Spec
 
-The full spec of the InferenceModel is defined [here](/reference/x-spec/#inferenceobjective).
\ No newline at end of file
+The full spec of the InferenceObjective is defined [here](/reference/x-v1-a2-spec/#inferenceobjective).
diff --git a/site-src/api-types/inferencepoolimport.md b/site-src/api-types/inferencepoolimport.md
new file mode 100644
index 0000000..aa58247
--- /dev/null
+++ b/site-src/api-types/inferencepoolimport.md
@@ -0,0 +1,26 @@
+# Inference Pool Import
+
+??? example "Alpha since v1.1.0"
+
+    The `InferencePoolImport` resource is alpha and may have breaking changes in
+    future releases of the API.
+
+## Background
+
+The **InferencePoolImport** API is a cluster-local, controller-managed resource that represents an imported InferencePool.
+It primarily communicates a relationship between an exported InferencePool and the exporting cluster name. It is not
+user-authored; status carries the effective import. Inference Platform Owners can reference the InferencePoolImport,
+even if the local cluster does not have an InferencePool. In the context of Gateway API, it means that an HTTPRoute can
+be configured to reference an InferencePoolImport to route matching requests to endpoints of backing InferencePools.
+
+Key ideas:
+
+- Map an exported InferencePool to exporting controller and cluster.
+- Name/namespace sameness with the exported InferencePool (avoids extra indirection).
+- Conditions: Surface a controller-level status condition to indicate whether the InferencePoolImport is ready for use.
+- Conditions: Surface parent-level status conditions to indicate whether the InferencePoolImport is referenced by a parent,
+  e.g. Gateway.
+
+## Spec
+
+The full spec of the InferencePoolImport is defined [here](/reference/x-v1a1-spec/#inferencepoolimport).
diff --git a/site-src/contributing/index.md b/site-src/contributing/index.md
index 7f56b82..eb5da17 100644
--- a/site-src/contributing/index.md
+++ b/site-src/contributing/index.md
@@ -46,4 +46,4 @@ doc. Feel free to add topics for discussion at an upcoming meeting.
 
 All meetings are recorded and automatically uploaded to the [WG-Serving meetings
 YouTube
-playlist](https://www.youtube.com/playlist?list=PL69nYSiGNLP30qNanabU75ayPK7OPNAAS).
+playlist](https://www.youtube.com/playlist?list=PL69nYSiGNLP2io2Gg92njBfh-DX9sk7O3).
diff --git a/site-src/guides/ga-migration.md b/site-src/guides/ga-migration.md
new file mode 100644
index 0000000..c56dea2
--- /dev/null
+++ b/site-src/guides/ga-migration.md
@@ -0,0 +1,274 @@
+# Inference Gateway: Migrating from v1alpha2 to v1 API
+
+## Introduction
+
+This guide provides a comprehensive walkthrough for migrating your Inference Gateway setup from the alpha `v1alpha2` API to the generally available `v1` API.
+This document is intended for platform administrators and networking specialists
+who are currently using the `v1alpha2` version of the Inference Gateway and
+want to upgrade to the `v1` version to leverage the latest features and improvements.
+
+Before you start the migration, ensure you are familiar with the concepts and deployment of the Inference Gateway.
+
+***
+
+## Before you begin
+
+Before starting the migration, it's important to determine if this guide is necessary for your setup.
+
+### Checking for Existing v1alpha2 APIs
+
+To check if you are actively using the `v1alpha2` Inference Gateway APIs, run the following command:
+
+```bash
+kubectl get inferencepools.inference.networking.x-k8s.io --all-namespaces
+```
+
+* If this command returns one or more `InferencePool` resources, you are using the `v1alpha2` API and should proceed with this migration guide.
+* If the command returns `No resources found`, you are not using the `v1alpha2` `InferencePool` and do not need to follow this migration guide. You can proceed with a fresh installation of the `v1` Inference Gateway.
+
+***
+
+## Migration Paths
+
+There are two paths for migrating from `v1alpha2` to `v1`:
+
+1.  **Simple Migration (with downtime):** This path is for users who can afford a short period of downtime. It involves deleting the old `v1alpha2` resources and CRDs before installing the new `v1` versions.
+2.  **Zero-Downtime Migration:** This path is for users who need to migrate without any service interruption. It involves running both `v1alpha2` and `v1` stacks side-by-side and gradually shifting traffic.
+
+***
+
+## Simple Migration (with downtime)
+
+This approach is faster and simpler but will result in a brief period of downtime while the resources are being updated. It is the recommended path if you do not require a zero-downtime migration.
+
+### 1. Delete Existing v1alpha2 Resources
+
+**Option a: Uninstall using Helm.**
+
+```bash
+helm uninstall <helm_alpha_inferencepool_name>
+```
+
+**Option b: Manually delete alpha `InferencePool` resources.**
+
+If you are not using Helm, you will need to manually delete all resources associated with your `v1alpha2` deployment. The key is to remove the `HTTPRoute`'s reference to the old `InferencePool` and then delete the `v1alpha2` resources themselves.
+
+1.  **Update or Delete the `HTTPRoute`**: Modify the `HTTPRoute` to remove the `backendRef` that points to the `v1alpha2` `InferencePool`.
+2.  **Delete the `InferencePool` and associated resources**: You must delete the `v1alpha2` `InferencePool`, any `InferenceModel` (or 'InferenceObjective') resources that point to it, and the corresponding Endpoint Picker (EPP) Deployment and Service.
+3.  **Delete the `v1alpha2` CRDs**: Once all `v1alpha2` custom resources are deleted, you can remove the CRD definitions from your cluster.
+    ```bash
+    # You can change the version to the one you installed `v1alpha2` CRDs
+    export VERSION="v0.3.0" 
+    kubectl delete -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/releases/download/${VERSION}/manifests.yaml
+    ```
+
+### 2. Install v1 Resources
+
+After cleaning up the old resources, you can proceed with a fresh installation of the `v1` Inference Gateway. 
+This involves deploying a new EPP image compatible with the `v1` API and installing the new `v1` CRDs. 
+You can then create a new v1 InferencePool with its corresponding InferenceObjective resources, and a new HTTPRoute that directs traffic to your new `v1` InferencePool.
+
+
+### 3. Verify the Deployment
+
+After a few minutes, verify that your new `v1` stack is correctly serving traffic. You should have a **`PROGRAMMED`** gateway.
+
+```bash
+❯ kubectl get gateway -o wide
+NAME                               CLASS            ADDRESS         PROGRAMMED   AGE
+<YOUR_INFERENCE_GATEWAY_NAME>   inference-gateway   <IP_ADDRESS>    True         10m
+```
+
+Curl the endpoint to make sure you are getting a successful response with a **200** response code.
+
+```bash
+IP=$(kubectl get gateway/<YOUR_INFERENCE_GATEWAY_NAME> -o jsonpath='{.status.addresses[0].value}')
+PORT=80
+
+curl -i ${IP}:${PORT}/v1/completions -H 'Content-Type: application/json' -d '{
+"model": "<your_model>",
+"prompt": "<your_prompt>",
+"max_tokens": 100,
+"temperature": 0
+}'
+```
+
+***
+
+## Zero-Downtime Migration
+
+This migration path is designed for users who cannot afford any service interruption. Assuming you already have the following stack shown in the diagram
+
+<img src="/images/alpha-stage.png" alt="Inference Gateway Alpha Stage" class="center" />
+
+### A Note on Interacting with Multiple API Versions
+
+During the zero-downtime migration, both `v1alpha2` and `v1` CRDs will be installed on your cluster. This can create ambiguity when using `kubectl` to query for `InferencePool` resources. To ensure you are interacting with the correct version, you **must** use the full resource name:
+
+* **For v1alpha2**: `kubectl get inferencepools.inference.networking.x-k8s.io`
+* **For v1**: `kubectl get inferencepools.inference.networking.k8s.io`
+
+The `v1` API also provides a convenient short name, `infpool`, which can be used to query `v1` resources specifically:
+
+```bash
+kubectl get infpool
+```
+
+This guide will use these full names or the short name for `v1` to avoid ambiguity.
+
+***
+
+### Stage 1: Side-by-side v1 Deployment
+
+In this stage, you will deploy the new `v1` `InferencePool` stack alongside the existing `v1alpha2` stack. This allows for a safe, gradual migration.
+
+After finishing all the steps in this stage, you’ll have the following infrastructure shown in the following diagram
+
+<img src="/images/migration-stage.png" alt="Inference Gateway Migration Stage" class="center" />
+
+**1. Install v1 CRDs**
+
+```bash
+RELEASE=v1.0.0
+kubectl apply -f [https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/$RELEASE/config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml](https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/$RELEASE/config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml)
+```
+
+**2. Install the v1 `InferencePool`**
+
+Use Helm to install a new `v1` `InferencePool` with a distinct release name (e.g., `vllm-llama3-8b-instruct-ga`).
+
+```bash
+helm install vllm-llama3-8b-instruct-ga \
+  --set inferencePool.modelServers.matchLabels.app=<the_label_you_used_for_the_model_server_deployment> \
+  --set provider.name=<YOUR_PROVIDER> \
+  --version $RELEASE \
+  oci://registry.k8s.io/gateway-api-inference-extension/charts/inferencepool
+```
+
+**3. Create the v1 `InferenceObjective`**
+
+The `v1` API replaces `InferenceModel` with `InferenceObjective`. Create the new resources, referencing the new `v1` `InferencePool`.
+
+```yaml
+kubectl apply -f - <<EOF
+---
+apiVersion: inference.networking.x-k8s.io/v1alpha2
+kind: InferenceObjective
+metadata:
+  name: food-review
+spec:
+  priority: 1
+  poolRef:
+    group: inference.networking.k8s.io
+    name: vllm-llama3-8b-instruct-ga
+---
+apiVersion: inference.networking.x-k8s.io/v1alpha2
+kind: InferenceObjective
+metadata:
+  name: base-model
+spec:
+  priority: 2
+  poolRef:
+    group: inference.networking.k8s.io
+    name: vllm-llama3-8b-instruct-ga
+---
+EOF
+```
+
+***
+
+### Stage 2: Traffic Shifting
+
+With both stacks running, you can start shifting traffic from `v1alpha2` to `v1` by updating the `HTTPRoute` to split traffic. This example shows a 50/50 split.
+
+**1. Update `HTTPRoute` for Traffic Splitting**
+
+```yaml
+kubectl apply -f - <<EOF
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: llm-route
+spec:
+  parentRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: inference-gateway
+  rules:
+  - backendRefs:
+    - group: inference.networking.x-k8s.io
+      kind: InferencePool
+      name: vllm-llama3-8b-instruct-alpha
+      weight: 50
+    - group: inference.networking.k8s.io
+      kind: InferencePool
+      name: vllm-llama3-8b-instruct-ga
+      weight: 50
+---
+EOF
+```
+
+**2. Verify and Monitor**
+
+After applying the changes, monitor the performance and stability of the new `v1` stack. Make sure the `inference-gateway` status `PROGRAMMED` is `True`.
+
+***
+
+### Stage 3: Finalization and Cleanup
+
+Once you have verified that the `v1` `InferencePool` is stable, you can direct all traffic to it and decommission the old `v1alpha2` resources.
+
+**1. Shift 100% of Traffic to the v1 `InferencePool`**
+
+Update the `HTTPRoute` to send all traffic to the `v1` pool.
+
+```yaml
+kubectl apply -f - <<EOF
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: llm-route
+spec:
+  parentRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: inference-gateway
+  rules:
+  - backendRefs:
+    - group: inference.networking.k8s.io
+      kind: InferencePool
+      name: vllm-llama3-8b-instruct-ga
+      weight: 100
+EOF
+```
+
+**2. Final Verification**
+
+Send test requests to ensure your `v1` stack is handling all traffic as expected.
+
+<img src="/images/ga-stage.png" alt="Inference Gateway GA Stage" class="center" />
+
+You should have a **`PROGRAMMED`** gateway:
+```bash
+❯ kubectl get gateway -o wide
+NAME                  CLASS                                ADDRESS         PROGRAMMED   AGE
+inference-gateway   inference-gateway   <IP_ADDRESS>    True         10m
+```
+
+Curl the endpoint and verify a **200** response code:
+```bash
+IP=$(kubectl get gateway/inference-gateway -o jsonpath='{.status.addresses[0].value}')
+PORT=80
+
+curl -i ${IP}:${PORT}/v1/completions -H 'Content-Type: application/json' -d '{
+"model": "<your_model>",
+"prompt": "<your_prompt>",
+"max_tokens": 100,
+"temperature": 0
+}'
+```
+
+**3. Clean Up v1alpha2 Resources**
+
+After confirming the `v1` stack is fully operational, safely remove the old `v1alpha2` resources.
diff --git a/site-src/guides/metrics-and-observability.md b/site-src/guides/metrics-and-observability.md
index 5e1f02e..8bcd952 100644
--- a/site-src/guides/metrics-and-observability.md
+++ b/site-src/guides/metrics-and-observability.md
@@ -53,6 +53,15 @@ This guide describes the current state of exposed metrics and how to scrape them
 |:---------------------------|:-----------------|:-------------------------------------------------|:------------------------------------------|:------------|
 | lora_syncer_adapter_status | Gauge            | Status of LoRA adapters (1=loaded, 0=not_loaded) | `adapter_name`=&lt;adapter-id&gt;         | ALPHA       |
 
+### Flow Control Metrics (Experimental)
+
+These metrics provide insights into the experimental flow control layer within the EPP.
+
+| **Metric name** | **Metric Type**  | <div style="width:200px">**Description**</div>  | <div style="width:250px">**Labels**</div> | **Status**  |
+|:---|:---|:---|:---|:---|
+| inference_extension_flow_control_request_queue_duration_seconds | Distribution | Distribution of the total time requests spend in the flow control layer. This is measured from the moment a request enters the `EnqueueAndWait` function until it reaches a final outcome (e.g., Dispatched, Rejected, Evicted). | `fairness_id`=&lt;flow-id&gt; <br> `priority`=&lt;flow-priority&gt; <br> `outcome`=&lt;QueueOutcome&gt; | ALPHA |
+| inference_extension_flow_control_queue_size | Gauge | The current number of requests being actively managed by the flow control layer. This counts requests from the moment they enter the `EnqueueAndWait` function until they reach a final outcome. | `fairness_id`=&lt;flow-id&gt; <br> `priority`=&lt;flow-priority&gt; | ALPHA |
+
 ## Scrape Metrics & Pprof profiles
 
 The metrics endpoints are exposed on different ports by default:
diff --git a/site-src/images/alpha-stage.png b/site-src/images/alpha-stage.png
new file mode 100644
index 0000000..7ba1ca5
Binary files /dev/null and b/site-src/images/alpha-stage.png differ
diff --git a/site-src/images/ga-stage.png b/site-src/images/ga-stage.png
new file mode 100644
index 0000000..5448c29
Binary files /dev/null and b/site-src/images/ga-stage.png differ
diff --git a/site-src/images/migration-stage.png b/site-src/images/migration-stage.png
new file mode 100644
index 0000000..9adac2f
Binary files /dev/null and b/site-src/images/migration-stage.png differ
diff --git a/site-src/implementations/gateways.md b/site-src/implementations/gateways.md
index 4ce052b..62abe01 100644
--- a/site-src/implementations/gateways.md
+++ b/site-src/implementations/gateways.md
@@ -8,6 +8,7 @@ This project has several implementations that are planned or in progress:
 * [Google Kubernetes Engine][4]
 * [Istio][5]
 * [Kgateway][6]
+* [Kubvernor][7]
 
 [1]:#agentgateway
 [2]:#alibaba-cloud-container-service-for-kubernetes
@@ -15,6 +16,7 @@ This project has several implementations that are planned or in progress:
 [4]:#google-kubernetes-engine
 [5]:#istio
 [6]:#kgateway
+[7]:#kubvernor
 
 ## Agentgateway
 
@@ -96,3 +98,11 @@ Issue](https://github.com/istio/istio/issues/55768).
 implementation that can run [independently](https://gateway-api-inference-extension.sigs.k8s.io/guides/#__tabbed_3_3), as an
 [Istio waypoint](https://kgateway.dev/blog/extend-istio-ambient-kgateway-waypoint/), or within your
 [llm-d infrastructure](https://github.com/llm-d-incubation/llm-d-infra) to improve accelerator (GPU) utilization for AI inference workloads.
+
+## Kubvernor
+
+[Kubvernor Rust API Gateway][krg] is an open-source, highly experimental implementation of API controller in Rust programming language. Currently, Kubvernor supports Envoy Proxy. The project aims to be as generic as possible so Kubvernor can be used to manage/deploy different gateways (Envoy, Nginx, HAProxy, etc.). See the docs for the [usage][krgu].
+
+[krg]:https://github.com/kubvernor/kubvernor
+[krgu]: https://github.com/kubvernor/kubvernor/blob/main/README.md
+
diff --git a/site-src/reference/spec.md b/site-src/reference/spec.md
index 11080fe..666ebe3 100644
--- a/site-src/reference/spec.md
+++ b/site-src/reference/spec.md
@@ -15,6 +15,32 @@ inference.networking.k8s.io API group.
 
 
 
+#### ControllerName
+
+_Underlying type:_ _string_
+
+ControllerName is the name of a controller that manages ParentStatus. It must be a domain prefixed
+path.
+
+Valid values include:
+
+* "example.com/bar"
+
+Invalid values include:
+
+* "example.com" - must include path
+* "foo.example.com" - must include path
+
+_Validation:_
+- MaxLength: 253
+- MinLength: 1
+- Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$`
+
+_Appears in:_
+- [ParentStatus](#parentstatus)
+
+
+
 #### EndpointPickerFailureMode
 
 _Underlying type:_ _string_
@@ -124,7 +150,7 @@ _Appears in:_
 | Field | Description | Default | Validation |
 | --- | --- | --- | --- |
 | `selector` _[LabelSelector](#labelselector)_ | Selector determines which Pods are members of this inference pool.<br />It matches Pods by their labels only within the same namespace; cross-namespace<br />selection is not supported.<br />The structure of this LabelSelector is intentionally simple to be compatible<br />with Kubernetes Service selectors, as some implementations may translate<br />this configuration into a Service resource. |  |  |
-| `targetPorts` _[Port](#port) array_ | TargetPorts defines a list of ports that are exposed by this InferencePool.<br />Currently, the list may only include a single port definition. |  | MaxItems: 1 <br />MinItems: 1 <br /> |
+| `targetPorts` _[Port](#port) array_ | TargetPorts defines a list of ports that are exposed by this InferencePool.<br />Every port will be treated as a distinctive endpoint by EPP,<br />addressable as a 'podIP:portNumber' combination. |  | MaxItems: 8 <br />MinItems: 1 <br /> |
 | `endpointPickerRef` _[EndpointPickerRef](#endpointpickerref)_ | EndpointPickerRef is a reference to the Endpoint Picker extension and its<br />associated configuration. |  |  |
 
 
@@ -329,6 +355,7 @@ _Appears in:_
 | --- | --- | --- | --- |
 | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | Conditions is a list of status conditions that provide information about the observed<br />state of the InferencePool. This field is required to be set by the controller that<br />manages the InferencePool.<br />Supported condition types are:<br />* "Accepted"<br />* "ResolvedRefs" |  | MaxItems: 8 <br /> |
 | `parentRef` _[ParentReference](#parentreference)_ | ParentRef is used to identify the parent resource that this status<br />is associated with. It is used to match the InferencePool with the parent<br />resource, such as a Gateway. |  |  |
+| `controllerName` _[ControllerName](#controllername)_ | ControllerName is a domain/path string that indicates the name of the controller that<br />wrote this status. This corresponds with the GatewayClass controllerName field when the<br />parentRef references a Gateway kind.<br />Example: "example.net/gateway-controller".<br />The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes names:<br /> https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names<br />Controllers MAY populate this field when writing status. When populating this field, controllers<br />should ensure that entries to status populated with their ControllerName are cleaned up when they<br />are no longer necessary. |  | MaxLength: 253 <br />MinLength: 1 <br />Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$` <br /> |
 
 
 #### Port
diff --git a/site-src/reference/x-v1a1-spec.md b/site-src/reference/x-v1a1-spec.md
new file mode 100644
index 0000000..55bec76
--- /dev/null
+++ b/site-src/reference/x-v1a1-spec.md
@@ -0,0 +1,126 @@
+# API Reference
+
+## Packages
+- [inference.networking.x-k8s.io/v1alpha1](#inferencenetworkingx-k8siov1alpha1)
+
+
+## inference.networking.x-k8s.io/v1alpha1
+
+Package v1alpha1 contains API Schema definitions for the
+inference.networking.x-k8s.io API group.
+
+
+### Resource Types
+- [InferencePoolImport](#inferencepoolimport)
+
+
+
+#### ClusterName
+
+_Underlying type:_ _string_
+
+ClusterName is the name of a cluster that exported the InferencePool.
+
+_Validation:_
+- MaxLength: 253
+- MinLength: 1
+
+_Appears in:_
+- [ExportingCluster](#exportingcluster)
+
+
+
+#### ControllerName
+
+_Underlying type:_ _string_
+
+ControllerName is the name of a controller that manages a resource. It must be a domain prefixed path.
+
+Valid values include:
+
+  - "example.com/bar"
+
+Invalid values include:
+
+  - "example.com" - must include path
+  - "foo.example.com" - must include path
+
+_Validation:_
+- MaxLength: 253
+- MinLength: 1
+- Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$`
+
+_Appears in:_
+- [ImportController](#importcontroller)
+
+
+
+#### ExportingCluster
+
+
+
+ExportingCluster defines a cluster that exported the InferencePool that backs this InferencePoolImport.
+
+
+
+_Appears in:_
+- [ImportController](#importcontroller)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _[ClusterName](#clustername)_ | Name of the exporting cluster (must be unique within the list). |  | MaxLength: 253 <br />MinLength: 1 <br />Required: \{\} <br /> |
+
+
+#### ImportController
+
+
+
+ImportController defines a controller that is responsible for managing the InferencePoolImport.
+
+
+
+_Appears in:_
+- [InferencePoolImportStatus](#inferencepoolimportstatus)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _[ControllerName](#controllername)_ | Name is a domain/path string that indicates the name of the controller that manages the<br />InferencePoolImport. Name corresponds to the GatewayClass controllerName field when the<br />controller will manage parents of type "Gateway". Otherwise, the name is implementation-specific.<br />Example: "example.net/import-controller".<br />The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are valid Kubernetes<br />names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).<br />A controller MUST populate this field when writing status and ensure that entries to status<br />populated with their controller name are removed when they are no longer necessary. |  | MaxLength: 253 <br />MinLength: 1 <br />Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$` <br /> |
+| `exportingClusters` _[ExportingCluster](#exportingcluster) array_ | ExportingClusters is a list of clusters that exported the InferencePool(s) that back the<br />InferencePoolImport. Required when the controller is responsible for CRUD'ing the InferencePoolImport<br />from the exported InferencePool(s). |  |  |
+| `parents` _ParentStatus array_ | Parents is a list of parent resources, typically Gateways, that are associated with the<br />InferencePoolImport, and the status of the InferencePoolImport with respect to each parent.<br />Ancestor would be a more accurate name, but Parent is consistent with InferencePool terminology.<br />Required when the controller manages the InferencePoolImport as an HTTPRoute backendRef. The controller<br />must add an entry for each parent it manages and remove the parent entry when the controller no longer<br />considers the InferencePoolImport to be associated with that parent. |  |  |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | Conditions track the state of the InferencePoolImport.<br />Known condition types are:<br /> * "Accepted" |  | MaxItems: 8 <br /> |
+
+
+#### InferencePoolImport
+
+
+
+InferencePoolImport is the Schema for the InferencePoolImports API.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `inference.networking.x-k8s.io/v1alpha1` | | |
+| `kind` _string_ | `InferencePoolImport` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. |  |  |
+| `status` _[InferencePoolImportStatus](#inferencepoolimportstatus)_ | Status defines the observed state of the InferencePoolImport. |  |  |
+
+
+#### InferencePoolImportStatus
+
+
+
+InferencePoolImportStatus defines the observed state of the InferencePoolImport.
+
+
+
+_Appears in:_
+- [InferencePoolImport](#inferencepoolimport)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `controllers` _[ImportController](#importcontroller) array_ | Controllers is a list of controllers that are responsible for managing the InferencePoolImport. |  | MaxItems: 8 <br />Required: \{\} <br /> |
+
+
diff --git a/site-src/reference/x-spec.md b/site-src/reference/x-v1a2-spec.md
similarity index 100%
rename from site-src/reference/x-spec.md
rename to site-src/reference/x-v1a2-spec.md
diff --git a/tools/dashboards/inference_gateway.json b/tools/dashboards/inference_gateway.json
index 50e4eb1..949b03f 100644
--- a/tools/dashboards/inference_gateway.json
+++ b/tools/dashboards/inference_gateway.json
@@ -36,7 +36,7 @@
           "showLineNumbers": false,
           "showMiniMap": false
         },
-        "content": "# Inferece Gateway Dashboard\n\nPlease see https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/pkg/epp/metrics for more details of underlying metrics used in the dashboard.",
+        "content": "# Inference Gateway Dashboard\n\nPlease see https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/pkg/epp/metrics for more details of underlying metrics used in the dashboard.",
         "mode": "markdown"
       },
       "pluginVersion": "10.2.4",



Test patch:
diff --git a/conformance/tests/epp_unavailable_fail_open.yaml b/conformance/tests/epp_unavailable_fail_open.yaml
index 9388a01..df3a6f0 100644
--- a/conformance/tests/epp_unavailable_fail_open.yaml
+++ b/conformance/tests/epp_unavailable_fail_open.yaml
@@ -2,13 +2,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-for-failopen-pool-gw
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-secondary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "secondary.example.com"
diff --git a/conformance/tests/gateway_following_epp_routing.go b/conformance/tests/gateway_following_epp_routing.go
index c320ad3..eaa6181 100644
--- a/conformance/tests/gateway_following_epp_routing.go
+++ b/conformance/tests/gateway_following_epp_routing.go
@@ -174,7 +174,7 @@ func assertTrafficOnlyReachesToExpectedPods(t *testing.T, suite *suite.Conforman
 			if err != nil {
 				return fmt.Errorf("failed to roundtrip request: %w", err)
 			}
-			if err := gwhttp.CompareRequest(t, &req, cReq, cRes, expected); err != nil {
+			if err := gwhttp.CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil {
 				return fmt.Errorf("response expectation failed for request: %w", err)
 			}
 
diff --git a/conformance/tests/gateway_following_epp_routing.yaml b/conformance/tests/gateway_following_epp_routing.yaml
index c4db638..d0f441a 100644
--- a/conformance/tests/gateway_following_epp_routing.yaml
+++ b/conformance/tests/gateway_following_epp_routing.yaml
@@ -2,13 +2,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-for-primary-gw
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "primary.example.com"
@@ -21,4 +21,3 @@ spec:
     - path:
         type: PathPrefix
         value: /primary-gateway-test
-  
\ No newline at end of file
diff --git a/conformance/tests/gateway_weighted_two_pools.go b/conformance/tests/gateway_weighted_two_pools.go
new file mode 100644
index 0000000..bbe3cbb
--- /dev/null
+++ b/conformance/tests/gateway_weighted_two_pools.go
@@ -0,0 +1,221 @@
+/*
+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
+
+import (
+	"fmt"
+	"math"
+	"net/http"
+	"strings"
+	"sync/atomic"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"golang.org/x/sync/errgroup"
+	"k8s.io/apimachinery/pkg/types"
+	gwhttp "sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+	"sigs.k8s.io/gateway-api/pkg/features"
+
+	"sigs.k8s.io/gateway-api-inference-extension/conformance/resources"
+	k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, GatewayWeightedAcrossTwoInferencePools)
+}
+
+// GatewayWeightedAcrossTwoInferencePools verifies that Gateway splits traffic across two
+// InferencePools according to backendRef weights, and that each request is routed to an
+// endpoint of the selected InferencePool.
+var GatewayWeightedAcrossTwoInferencePools = suite.ConformanceTest{
+	ShortName:   "GatewayWeightedAcrossTwoInferencePools",
+	Description: "Gateway should split traffic across two InferencePools based on backendRef weights and route only to endpoints of the selected InferencePool",
+	Manifests:   []string{"tests/gateway_weighted_two_pools.yaml"},
+	Features: []features.FeatureName{
+		features.SupportGateway,
+		features.FeatureName("SupportInferencePool"),
+	},
+	Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
+		const (
+			hostname = "primary.example.com"
+			path     = "/weighted-two-pools-test"
+
+			// Sample size so the weight signal dominates random noise.
+			totalRequests      = 200
+			concurrentRequests = 5
+
+			// These route weights must match the test manifest.
+			primaryWeight   = 70
+			secondaryWeight = 30
+		)
+
+		// Objects under test.
+		httpRouteNN := types.NamespacedName{Name: "httproute-weighted-two-pools", Namespace: resources.AppBackendNamespace}
+		gatewayNN := resources.PrimaryGatewayNN
+		primaryPoolNN := resources.PrimaryInferencePoolNN
+		secondaryPoolNN := types.NamespacedName{Name: "secondary-inference-pool", Namespace: resources.AppBackendNamespace}
+
+		// Labels for the two deployments defined in base.yaml.
+		primaryLabels := map[string]string{"app": "primary-inference-model-server"}
+		secondaryLabels := map[string]string{"app": "secondary-inference-model-server"}
+
+		t.Log("Verifying HTTPRoute and both InferencePools are accepted and the Gateway has an address.")
+		k8sutils.HTTPRouteMustBeAcceptedAndResolved(t, s.Client, s.TimeoutConfig, httpRouteNN, gatewayNN)
+		k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, primaryPoolNN, gatewayNN)
+		k8sutils.InferencePoolMustBeAcceptedByParent(t, s.Client, secondaryPoolNN, gatewayNN)
+		gwAddr := k8sutils.GetGatewayEndpoint(t, s.Client, s.TimeoutConfig, gatewayNN)
+
+		// Discover pods for each pool and build quick lookup sets.
+		t.Logf("Fetching primary backend pods with labels: %v", primaryLabels)
+		primaryPods, err := k8sutils.GetPodsWithLabel(t, s.Client, resources.AppBackendNamespace, primaryLabels, s.TimeoutConfig)
+		require.NoError(t, err)
+		require.Len(t, primaryPods, 3) // base.yaml uses 3 replicas
+
+		t.Logf("Fetching secondary backend pods with labels: %v", secondaryLabels)
+		secondaryPods, err := k8sutils.GetPodsWithLabel(t, s.Client, resources.AppBackendNamespace, secondaryLabels, s.TimeoutConfig)
+		require.NoError(t, err)
+		require.Len(t, secondaryPods, 3) // base.yaml uses 3 replicas
+
+		primaryPodNames := make([]string, 0, len(primaryPods))
+		primaryPodIPs := make([]string, 0, len(primaryPods))
+		for _, p := range primaryPods {
+			require.NotEmpty(t, p.Status.PodIP, "primary pod %s has no IP yet", p.Name)
+			primaryPodNames = append(primaryPodNames, p.Name)
+			primaryPodIPs = append(primaryPodIPs, p.Status.PodIP)
+		}
+
+		secondaryPodNames := make([]string, 0, len(secondaryPods))
+		secondaryPodIPs := make([]string, 0, len(secondaryPods))
+		for _, p := range secondaryPods {
+			require.NotEmpty(t, p.Status.PodIP, "secondary pod %s has no IP yet", p.Name)
+			secondaryPodNames = append(secondaryPodNames, p.Name)
+			secondaryPodIPs = append(secondaryPodIPs, p.Status.PodIP)
+		}
+
+		// Send one targeted request per backend Pod to ensure EPP readiness.
+		allIPs := append(append([]string{}, primaryPodIPs...), secondaryPodIPs...)
+		allNames := append(append([]string{}, primaryPodNames...), secondaryPodNames...)
+		for i := 0; i < len(allIPs); i++ {
+			traffic.MakeRequestAndExpectSuccess(
+				t,
+				s.RoundTripper,
+				s.TimeoutConfig,
+				gwAddr,
+				traffic.Request{
+					Host: hostname,
+					Path: path,
+					Headers: map[string]string{
+						test.HeaderTestEppEndPointSelectionKey: allIPs[i],
+					},
+					Method:    http.MethodPost,
+					Body:      `{"model":"conformance-fake-model","prompt":"Warmup"}`,
+					Backend:   allNames[i],
+					Namespace: resources.AppBackendNamespace,
+				},
+			)
+		}
+
+		// Provide a union list of eligible endpoints for the test. Each pool's EPP
+		// should filter to endpoints that actually belong to its pool.
+		eppHeaderValue := strings.Join(allIPs, ",")
+
+		requestBody := `{
+			"model": "conformance-fake-model",
+			"prompt": "Write as if you were a critic: San Francisco"
+		}`
+
+		// Build quick lookup sets for attributing each hit to a pool by backend pod name.
+		primarySet := make(map[string]struct{}, len(primaryPodNames))
+		for _, n := range primaryPodNames {
+			primarySet[n] = struct{}{}
+		}
+		secondarySet := make(map[string]struct{}, len(secondaryPodNames))
+		for _, n := range secondaryPodNames {
+			secondarySet[n] = struct{}{}
+		}
+
+		headers := map[string]string{
+			test.HeaderTestEppEndPointSelectionKey: eppHeaderValue,
+		}
+		expected := gwhttp.ExpectedResponse{
+			Request: gwhttp.Request{
+				Host:    hostname,
+				Path:    path,
+				Method:  http.MethodPost,
+				Headers: headers,
+			},
+			Response: gwhttp.Response{
+				StatusCode: http.StatusOK,
+			},
+			Namespace: resources.AppBackendNamespace,
+		}
+		req := gwhttp.MakeRequest(t, &expected, gwAddr, "HTTP", "http")
+
+		var primaryHits, secondaryHits atomic.Int64
+		var g errgroup.Group
+		g.SetLimit(concurrentRequests)
+
+		for i := 0; i < totalRequests; i++ {
+			g.Go(func() error {
+				cReq, cRes, err := traffic.MakeCallRoundTripper(t, s.RoundTripper, &traffic.RequestWithBody{
+					Request: req,
+					Body:    strings.NewReader(requestBody),
+				})
+				if err != nil {
+					return fmt.Errorf("failed to roundtrip request: %w", err)
+				}
+				if err := gwhttp.CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil {
+					return fmt.Errorf("response expectation failed: %w", err)
+				}
+
+				// Attribute response to pool by backend pod name.
+				if _, ok := primarySet[cReq.Pod]; ok {
+					primaryHits.Add(1)
+				} else if _, ok := secondarySet[cReq.Pod]; ok {
+					secondaryHits.Add(1)
+				} else {
+					return fmt.Errorf("request was handled by unexpected pod %q (not in either pool)", cReq.Pod)
+				}
+				return nil
+			})
+		}
+		require.NoError(t, g.Wait(), "requests failed")
+
+		ph := float64(primaryHits.Load())
+		sh := float64(secondaryHits.Load())
+		total := ph + sh
+		require.Equal(t, int64(totalRequests), int64(total), "sum of hits must equal number of attempts")
+		require.Greater(t, total, 0.0)
+
+		observedPrimary := ph / total
+		expectedPrimary := float64(primaryWeight) / float64(primaryWeight+secondaryWeight)
+
+		// Allow either a 10 percentage-point absolute error, or a 3-sigma binomial CI.
+		sigma := math.Sqrt(expectedPrimary * (1.0 - expectedPrimary) / total)
+		absTolerance := math.Max(0.10, 3.0*sigma)
+
+		diff := math.Abs(observedPrimary - expectedPrimary)
+		require.LessOrEqualf(t, diff, absTolerance,
+			"weighted split out of bounds: observed primary=%.3f (hits=%d/%d), expected=%.3f, tolerance=±%.3f",
+			observedPrimary, int64(ph), int64(total), expectedPrimary, absTolerance)
+		t.Logf("Weighted split OK: primary=%.3f (hits=%d/%d), expected=%.3f, tolerance=±%.3f; secondary hits=%d",
+			observedPrimary, int64(ph), int64(total), expectedPrimary, absTolerance, int64(sh))
+	},
+}
diff --git a/conformance/tests/gateway_weighted_two_pools.yaml b/conformance/tests/gateway_weighted_two_pools.yaml
new file mode 100644
index 0000000..2882822
--- /dev/null
+++ b/conformance/tests/gateway_weighted_two_pools.yaml
@@ -0,0 +1,30 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: httproute-weighted-two-pools
+  namespace: gateway-conformance-app-backend
+spec:
+  parentRefs:
+  - group: gateway.networking.k8s.io
+    kind: Gateway
+    name: conformance-primary
+    namespace: gateway-conformance-infra
+    sectionName: http
+  hostnames:
+  - "primary.example.com"
+  rules:
+  - matches:
+    - path:
+        type: PathPrefix
+        value: /weighted-two-pools-test
+    backendRefs:
+    # 70% of traffic goes to the primary pool
+    - group: inference.networking.k8s.io
+      kind: InferencePool
+      name: primary-inference-pool
+      weight: 70
+    # 30% of traffic goes to the secondary pool
+    - group: inference.networking.k8s.io
+      kind: InferencePool
+      name: secondary-inference-pool
+      weight: 30
diff --git a/conformance/tests/httproute_invalid_inferencepool_ref.yaml b/conformance/tests/httproute_invalid_inferencepool_ref.yaml
index 15e7ad5..a0954cd 100644
--- a/conformance/tests/httproute_invalid_inferencepool_ref.yaml
+++ b/conformance/tests/httproute_invalid_inferencepool_ref.yaml
@@ -2,13 +2,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-to-non-existent-pool
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   rules:
   - backendRefs:
diff --git a/conformance/tests/httproute_multiple_gateways_different_pools.yaml b/conformance/tests/httproute_multiple_gateways_different_pools.yaml
index caded16..17bf494 100644
--- a/conformance/tests/httproute_multiple_gateways_different_pools.yaml
+++ b/conformance/tests/httproute_multiple_gateways_different_pools.yaml
@@ -3,12 +3,12 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: route-for-primary-gateway
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
   hostnames:
   - "primary.example.com"
   rules:
@@ -25,12 +25,12 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: route-for-secondary-gateway
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - kind: Gateway
     name: conformance-secondary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
   hostnames:
   - "secondary.example.com"
   rules:
diff --git a/conformance/tests/inferencepool_accepted.yaml b/conformance/tests/inferencepool_accepted.yaml
index 59710ba..b155ae2 100644
--- a/conformance/tests/inferencepool_accepted.yaml
+++ b/conformance/tests/inferencepool_accepted.yaml
@@ -3,20 +3,20 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-for-inferencepool-accepted
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   rules:
   - backendRefs:
     - group: inference.networking.k8s.io
       kind: InferencePool
       name: primary-inference-pool
-      # namespace: gateway-conformance-app-backend - is omitted since it is in the same namespace as HTTPRoute
+      # namespace: inference-conformance-app-backend - is omitted since it is in the same namespace as HTTPRoute
     matches:
     - path:
         type: PathPrefix
diff --git a/conformance/tests/inferencepool_httproute_port_validation.yaml b/conformance/tests/inferencepool_httproute_port_validation.yaml
index 9c78117..53d8455 100644
--- a/conformance/tests/inferencepool_httproute_port_validation.yaml
+++ b/conformance/tests/inferencepool_httproute_port_validation.yaml
@@ -4,13 +4,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-pool-port-unspecified
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "port-unspecified.example.com"
@@ -30,13 +30,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-pool-port-matching
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "port-matching.example.com"
@@ -56,13 +56,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-pool-port-non-matching
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "port-non-matching.example.com"
diff --git a/conformance/tests/inferencepool_invalid_epp_service.yaml b/conformance/tests/inferencepool_invalid_epp_service.yaml
index b3dc70e..3f3b8c4 100644
--- a/conformance/tests/inferencepool_invalid_epp_service.yaml
+++ b/conformance/tests/inferencepool_invalid_epp_service.yaml
@@ -2,7 +2,7 @@ apiVersion: inference.networking.k8s.io/v1
 kind: InferencePool
 metadata:
   name: pool-with-invalid-epp
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   selector:
     matchLabels:
@@ -19,11 +19,11 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-for-invalid-epp-pool
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
   rules:
   - backendRefs:
     - name: pool-with-invalid-epp
diff --git a/conformance/tests/inferencepool_multiple_rules_different_pools.yaml b/conformance/tests/inferencepool_multiple_rules_different_pools.yaml
index 2dd8f4a..0539a11 100644
--- a/conformance/tests/inferencepool_multiple_rules_different_pools.yaml
+++ b/conformance/tests/inferencepool_multiple_rules_different_pools.yaml
@@ -3,11 +3,11 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-multiple-rules-different-pools
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
     - name: conformance-primary
-      namespace: gateway-conformance-infra
+      namespace: inference-conformance-infra
   rules:
     - matches:
         - path:
diff --git a/conformance/tests/inferencepool_resolvedrefs_condition.yaml b/conformance/tests/inferencepool_resolvedrefs_condition.yaml
index 8947168..05a2318 100644
--- a/conformance/tests/inferencepool_resolvedrefs_condition.yaml
+++ b/conformance/tests/inferencepool_resolvedrefs_condition.yaml
@@ -8,13 +8,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-for-primary-gw
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-primary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "primary.example.com"
@@ -33,13 +33,13 @@ apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: httproute-for-secondary-gw
-  namespace: gateway-conformance-app-backend
+  namespace: inference-conformance-app-backend
 spec:
   parentRefs:
   - group: gateway.networking.k8s.io
     kind: Gateway
     name: conformance-secondary
-    namespace: gateway-conformance-infra
+    namespace: inference-conformance-infra
     sectionName: http
   hostnames:
   - "secondary.example.com"
diff --git a/pkg/epp/controller/inferencepool_reconciler_test.go b/pkg/epp/controller/inferencepool_reconciler_test.go
index 7f69385..553fcc7 100644
--- a/pkg/epp/controller/inferencepool_reconciler_test.go
+++ b/pkg/epp/controller/inferencepool_reconciler_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
@@ -180,7 +181,9 @@ type diffStoreParams struct {
 
 func diffStore(datastore datastore.Datastore, params diffStoreParams) string {
 	gotPool, _ := datastore.PoolGet()
-	if diff := cmp.Diff(params.wantPool, gotPool); diff != "" {
+	// controller-runtime fake client may not populate TypeMeta (APIVersion/Kind).
+	// Ignore it when comparing pools.
+	if diff := cmp.Diff(params.wantPool, gotPool, cmpopts.IgnoreTypes(metav1.TypeMeta{})); diff != "" {
 		return "pool:" + diff
 	}
 
@@ -333,7 +336,10 @@ func xDiffStore(t *testing.T, datastore datastore.Datastore, params xDiffStorePa
 	if err != nil {
 		t.Fatalf("failed to convert InferencePool to XInferencePool: %v", err)
 	}
-	if diff := cmp.Diff(params.wantPool, gotXPool); diff != "" {
+
+	// controller-runtime fake client may not populate TypeMeta (APIVersion/Kind).
+	// Ignore it when comparing pools.
+	if diff := cmp.Diff(params.wantPool, gotXPool, cmpopts.IgnoreTypes(metav1.TypeMeta{})); diff != "" {
 		return "pool:" + diff
 	}
 
diff --git a/pkg/epp/flowcontrol/controller/controller_test.go b/pkg/epp/flowcontrol/controller/controller_test.go
index c832e2d..74802f2 100644
--- a/pkg/epp/flowcontrol/controller/controller_test.go
+++ b/pkg/epp/flowcontrol/controller/controller_test.go
@@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// Note on Time-Based Lifecycle Tests:
+// Tests validating the controller's handling of request TTLs (e.g., OnReqCtxTimeout*) rely on real-time timers
+// (context.WithDeadline). The injected testclock.FakeClock is used to control the timing of internal loops (like
+// reconciliation), but it cannot manipulate the timers used by the standard context package. Therefore, these specific
+// tests use time.Sleep or assertions on real-time durations.
+
 package controller
 
 import (
@@ -68,23 +74,40 @@ func withShardProcessorFactory(factory shardProcessorFactory) flowControllerOpti
 
 // testHarness holds the `FlowController` and its dependencies under test.
 type testHarness struct {
-	fc                   *FlowController
-	cfg                  Config
-	mockRegistry         *mockRegistryClient
-	mockDetector         *mocks.MockSaturationDetector
+	fc  *FlowController
+	cfg Config
+	// clock is the clock interface used by the controller.
+	clock        clock.WithTicker
+	mockRegistry *mockRegistryClient
+	mockDetector *mocks.MockSaturationDetector
+	// mockClock provides access to FakeClock methods (Step, HasWaiters) if and only if the underlying clock is a
+	// FakeClock.
 	mockClock            *testclock.FakeClock
 	mockProcessorFactory *mockShardProcessorFactory
 }
 
 // newUnitHarness creates a test environment with a mock processor factory, suitable for focused unit tests of the
-// controller's logic. It starts the controller's run loop and returns a cancel function to stop it.
+// controller's logic. It starts the controller's run loop using the provided context for lifecycle management.
 func newUnitHarness(t *testing.T, ctx context.Context, cfg Config, registry *mockRegistryClient) *testHarness {
 	t.Helper()
 	mockDetector := &mocks.MockSaturationDetector{}
+
+	// Initialize the FakeClock with the current system time.
+	// The controller implementation uses the injected clock to calculate the deadline timestamp,vbut uses the standard
+	// context.WithDeadline (which relies on the system clock) to enforce it.
+	// If the FakeClock's time is far from the system time, deadlines calculated based on the FakeClockvmight already be
+	// expired according to the system clock, causing immediate TTL failures.
 	mockClock := testclock.NewFakeClock(time.Now())
+
 	mockProcessorFactory := &mockShardProcessorFactory{
 		processors: make(map[string]*mockShardProcessor),
 	}
+
+	// Default the registry if nil, simplifying tests that don't focus on registry interaction.
+	if registry == nil {
+		registry = &mockRegistryClient{}
+	}
+
 	opts := []flowControllerOption{
 		withRegistryClient(registry),
 		withClock(mockClock),
@@ -96,6 +119,7 @@ func newUnitHarness(t *testing.T, ctx context.Context, cfg Config, registry *moc
 	h := &testHarness{
 		fc:                   fc,
 		cfg:                  cfg,
+		clock:                mockClock,
 		mockRegistry:         registry,
 		mockDetector:         mockDetector,
 		mockClock:            mockClock,
@@ -109,7 +133,13 @@ func newUnitHarness(t *testing.T, ctx context.Context, cfg Config, registry *moc
 func newIntegrationHarness(t *testing.T, ctx context.Context, cfg Config, registry *mockRegistryClient) *testHarness {
 	t.Helper()
 	mockDetector := &mocks.MockSaturationDetector{}
+	// Align FakeClock with system time. See explanation in newUnitHarness.
+
 	mockClock := testclock.NewFakeClock(time.Now())
+	if registry == nil {
+		registry = &mockRegistryClient{}
+	}
+
 	opts := []flowControllerOption{
 		withRegistryClient(registry),
 		withClock(mockClock),
@@ -120,6 +150,7 @@ func newIntegrationHarness(t *testing.T, ctx context.Context, cfg Config, regist
 	h := &testHarness{
 		fc:           fc,
 		cfg:          cfg,
+		clock:        mockClock,
 		mockRegistry: registry,
 		mockDetector: mockDetector,
 		mockClock:    mockClock,
@@ -166,9 +197,11 @@ func (m *mockRegistryClient) ShardStats() []contracts.ShardStats {
 type mockShardProcessor struct {
 	SubmitFunc        func(item *internal.FlowItem) error
 	SubmitOrBlockFunc func(ctx context.Context, item *internal.FlowItem) error
-	runCtx            context.Context
-	runCtxMu          sync.RWMutex
-	runStarted        chan struct{}
+	// runCtx captures the context provided to the Run method for lifecycle assertions.
+	runCtx   context.Context
+	runCtxMu sync.RWMutex
+	// runStarted is closed when the Run method is called, allowing tests to synchronize with worker startup.
+	runStarted chan struct{}
 }
 
 func (m *mockShardProcessor) Submit(item *internal.FlowItem) error {
@@ -192,9 +225,11 @@ func (m *mockShardProcessor) Run(ctx context.Context) {
 	if m.runStarted != nil {
 		close(m.runStarted)
 	}
+	// Block until the context is cancelled, simulating a running worker.
 	<-ctx.Done()
 }
 
+// Context returns the context captured during the Run method call.
 func (m *mockShardProcessor) Context() context.Context {
 	m.runCtxMu.RLock()
 	defer m.runCtxMu.RUnlock()
@@ -207,10 +242,12 @@ type mockShardProcessorFactory struct {
 	processors map[string]*mockShardProcessor
 }
 
+// new is the factory function conforming to the `shardProcessorFactory` signature.
 func (f *mockShardProcessorFactory) new(
+	_ context.Context, // The factory does not use the lifecycle context; it's passed to the processor's Run method later.
 	shard contracts.RegistryShard,
 	_ contracts.SaturationDetector,
-	_ clock.Clock,
+	_ clock.WithTicker,
 	_ time.Duration,
 	_ int,
 	_ logr.Logger,
@@ -220,7 +257,7 @@ func (f *mockShardProcessorFactory) new(
 	if proc, ok := f.processors[shard.ID()]; ok {
 		return proc
 	}
-	// Return a default mock processor if one is not registered.
+	// Return a default mock processor if one is not explicitly registered by the test.
 	return &mockShardProcessor{}
 }
 
@@ -262,9 +299,8 @@ func (b *mockShardBuilder) build() contracts.RegistryShard {
 
 var defaultFlowKey = types.FlowKey{ID: "test-flow", Priority: 100}
 
-func newTestRequest(ctx context.Context, key types.FlowKey) *typesmocks.MockFlowControlRequest {
+func newTestRequest(key types.FlowKey) *typesmocks.MockFlowControlRequest {
 	return &typesmocks.MockFlowControlRequest{
-		Ctx:       ctx,
 		FlowKeyV:  key,
 		ByteSizeV: 100,
 		IDV:       "req-" + key.ID,
@@ -273,6 +309,8 @@ func newTestRequest(ctx context.Context, key types.FlowKey) *typesmocks.MockFlow
 
 // --- Test Cases ---
 
+// TestFlowController_EnqueueAndWait covers the primary API entry point, focusing on validation, distribution logic,
+// retries, and the request lifecycle (including post-distribution cancellation/timeout).
 func TestFlowController_EnqueueAndWait(t *testing.T) {
 	t.Parallel()
 
@@ -281,37 +319,80 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 
 		t.Run("OnNilRequest", func(t *testing.T) {
 			t.Parallel()
-			h := newUnitHarness(t, t.Context(), Config{}, &mockRegistryClient{})
+			h := newUnitHarness(t, t.Context(), Config{}, nil)
 
-			outcome, err := h.fc.EnqueueAndWait(nil)
+			outcome, err := h.fc.EnqueueAndWait(context.Background(), nil)
 			require.Error(t, err, "EnqueueAndWait must reject a nil request")
-			assert.Equal(t, "request cannot be nil", err.Error())
-			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome, "outcome should be QueueOutcomeRejectedOther")
+			assert.Equal(t, "request cannot be nil", err.Error(), "error message must be specific")
+			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome,
+				"outcome should be QueueOutcomeRejectedOther for invalid inputs")
 		})
 
+		t.Run("OnReqCtxExpiredBeforeDistribution", func(t *testing.T) {
+			t.Parallel()
+			// Test that if the request context provided to EnqueueAndWait is already expired, it returns immediately.
+			h := newUnitHarness(t, t.Context(), Config{DefaultRequestTTL: 1 * time.Minute}, nil)
+
+			// Configure registry to return a shard.
+			shardA := newMockShard("shard-A").build()
+			h.mockRegistry.WithConnectionFunc = func(_ types.FlowKey, fn func(_ contracts.ActiveFlowConnection) error) error {
+				return fn(&mockActiveFlowConnection{ActiveShardsV: []contracts.RegistryShard{shardA}})
+			}
+			// Configure processor to block until context expiry.
+			h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
+				SubmitFunc: func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
+				SubmitOrBlockFunc: func(ctx context.Context, _ *internal.FlowItem) error {
+					<-ctx.Done()              // Wait for the context to be done.
+					return context.Cause(ctx) // Return the cause.
+				},
+			}
+
+			req := newTestRequest(defaultFlowKey)
+			// Use a context with a deadline in the past.
+			reqCtx, cancel := context.WithDeadlineCause(
+				context.Background(),
+				h.clock.Now().Add(-1*time.Second),
+				types.ErrTTLExpired)
+			defer cancel()
+
+			outcome, err := h.fc.EnqueueAndWait(reqCtx, req)
+			require.Error(t, err, "EnqueueAndWait must fail if request context deadline is exceeded")
+			assert.ErrorIs(t, err, types.ErrRejected, "error should wrap ErrRejected")
+			assert.ErrorIs(t, err, types.ErrTTLExpired, "error should wrap types.ErrTTLExpired from the context cause")
+			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome, "outcome should be QueueOutcomeRejectedOther")
+		})
 		t.Run("OnControllerShutdown", func(t *testing.T) {
 			t.Parallel()
+			// Create a context specifically for the controller's lifecycle.
 			ctx, cancel := context.WithCancel(t.Context())
-			h := newUnitHarness(t, ctx, Config{}, &mockRegistryClient{})
+			h := newUnitHarness(t, ctx, Config{}, nil)
 			cancel() // Immediately stop the controller.
 
-			req := newTestRequest(context.Background(), defaultFlowKey)
-			outcome, err := h.fc.EnqueueAndWait(req)
+			// Wait for the controller's run loop and all workers (none in this case) to exit.
+			// We need to wait because the shutdown process is asynchronous.
+			h.fc.wg.Wait()
+
+			req := newTestRequest(defaultFlowKey)
+			// The request context is valid, but the controller itself is stopped.
+			outcome, err := h.fc.EnqueueAndWait(context.Background(), req)
 			require.Error(t, err, "EnqueueAndWait must reject requests if controller is not running")
 			assert.ErrorIs(t, err, types.ErrRejected, "error should wrap ErrRejected")
 			assert.ErrorIs(t, err, types.ErrFlowControllerNotRunning, "error should wrap ErrFlowControllerNotRunning")
-			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome, "outcome should be QueueOutcomeRejectedOther")
+			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome,
+				"outcome should be QueueOutcomeRejectedOther on shutdown")
 		})
 
 		t.Run("OnNoShardsAvailable", func(t *testing.T) {
 			t.Parallel()
-			h := newUnitHarness(t, t.Context(), Config{}, &mockRegistryClient{})
+			// The default mockRegistryClient returns an empty list of ActiveShards.
+			h := newUnitHarness(t, t.Context(), Config{}, nil)
 
-			req := newTestRequest(context.Background(), defaultFlowKey)
-			outcome, err := h.fc.EnqueueAndWait(req)
+			req := newTestRequest(defaultFlowKey)
+			outcome, err := h.fc.EnqueueAndWait(context.Background(), req)
 			require.Error(t, err, "EnqueueAndWait must reject requests if no shards are available")
 			assert.ErrorIs(t, err, types.ErrRejected, "error should wrap ErrRejected")
-			assert.Equal(t, types.QueueOutcomeRejectedCapacity, outcome, "outcome should be QueueOutcomeRejectedCapacity")
+			assert.Equal(t, types.QueueOutcomeRejectedCapacity, outcome,
+				"outcome should be QueueOutcomeRejectedCapacity when no shards exist for the flow")
 		})
 
 		t.Run("OnRegistryConnectionError", func(t *testing.T) {
@@ -319,7 +400,8 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 			mockRegistry := &mockRegistryClient{}
 			h := newUnitHarness(t, t.Context(), Config{}, mockRegistry)
 
-			expectedErr := errors.New("connection failed")
+			expectedErr := errors.New("simulated connection failure")
+			// Configure the registry to fail when attempting to retrieve ActiveFlowConnection.
 			mockRegistry.WithConnectionFunc = func(
 				_ types.FlowKey,
 				_ func(conn contracts.ActiveFlowConnection) error,
@@ -327,23 +409,27 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 				return expectedErr
 			}
 
-			req := newTestRequest(context.Background(), defaultFlowKey)
-			outcome, err := h.fc.EnqueueAndWait(req)
+			req := newTestRequest(defaultFlowKey)
+			outcome, err := h.fc.EnqueueAndWait(context.Background(), req)
 			require.Error(t, err, "EnqueueAndWait must reject requests if registry connection fails")
 			assert.ErrorIs(t, err, types.ErrRejected, "error should wrap ErrRejected")
 			assert.ErrorIs(t, err, expectedErr, "error should wrap the underlying connection error")
-			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome, "outcome should be QueueOutcomeRejectedOther")
+			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome,
+				"outcome should be QueueOutcomeRejectedOther for transient registry errors")
 		})
 
+		// This test validates the documented invariant handling in distributeRequest.
 		t.Run("PanicsOnManagedQueueError", func(t *testing.T) {
 			t.Parallel()
 			mockRegistry := &mockRegistryClient{}
 			h := newUnitHarness(t, t.Context(), Config{}, mockRegistry)
 
+			// Create a faulty shard that successfully leases the flow but fails to return the
+			// ManagedQueue.
 			faultyShard := &mocks.MockRegistryShard{
 				IDFunc: func() string { return "faulty-shard" },
 				ManagedQueueFunc: func(_ types.FlowKey) (contracts.ManagedQueue, error) {
-					return nil, errors.New("queue retrieval failed")
+					return nil, errors.New("invariant violation: queue retrieval failed")
 				},
 			}
 			mockRegistry.WithConnectionFunc = func(
@@ -353,22 +439,30 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 				return fn(&mockActiveFlowConnection{ActiveShardsV: []contracts.RegistryShard{faultyShard}})
 			}
 
-			req := newTestRequest(context.Background(), defaultFlowKey)
+			req := newTestRequest(defaultFlowKey)
 			assert.Panics(t, func() {
-				_, _ = h.fc.EnqueueAndWait(req)
-			}, "EnqueueAndWait did not panic as expected on a ManagedQueue error")
+				_, _ = h.fc.EnqueueAndWait(context.Background(), req)
+			}, "EnqueueAndWait must panic when a registry implementation violates the ManagedQueue contract")
 		})
 	})
 
+	// Distribution tests validate the JSQ-Bytes algorithm, the two-phase submission strategy, and error handling during
+	// the handoff, including time-based failures during blocking fallback.
 	t.Run("Distribution", func(t *testing.T) {
 		t.Parallel()
 
+		// Define a long default TTL to prevent unexpected timeouts unless a test case explicitly sets a shorter one.
+		const defaultTestTTL = 5 * time.Second
+
 		testCases := []struct {
 			name            string
 			shards          []contracts.RegistryShard
 			setupProcessors func(t *testing.T, h *testHarness)
+			// requestTTL overrides the default TTL for time-sensitive tests.
+			requestTTL      time.Duration
 			expectedOutcome types.QueueOutcome
 			expectErr       bool
+			expectErrIs     error
 		}{
 			{
 				name:   "SubmitSucceeds_NonBlocking_WithSingleActiveShard",
@@ -376,7 +470,8 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 				setupProcessors: func(t *testing.T, h *testHarness) {
 					h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
 						SubmitFunc: func(item *internal.FlowItem) error {
-							go item.Finalize(types.QueueOutcomeDispatched, nil)
+							// Simulate asynchronous processing and successful dispatch.
+							go item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
 							return nil
 						},
 					}
@@ -386,19 +481,20 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 			{
 				name: "DistributesToLeastLoadedShard_WithMultipleActiveShards",
 				shards: []contracts.RegistryShard{
-					newMockShard("shard-A").withByteSize(1000).build(),
-					newMockShard("shard-B").withByteSize(100).build(),
+					newMockShard("shard-A").withByteSize(1000).build(), // More loaded
+					newMockShard("shard-B").withByteSize(100).build(),  // Least loaded
 				},
 				setupProcessors: func(t *testing.T, h *testHarness) {
 					h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
 						SubmitFunc: func(_ *internal.FlowItem) error {
-							t.Error("Submit was called on the more loaded shard (shard-A), which is incorrect")
+							t.Error("Submit was called on the more loaded shard (shard-A); JSQ-Bytes algorithm failed")
 							return internal.ErrProcessorBusy
 						},
 					}
 					h.mockProcessorFactory.processors["shard-B"] = &mockShardProcessor{
 						SubmitFunc: func(item *internal.FlowItem) error {
-							go item.Finalize(types.QueueOutcomeDispatched, nil)
+							item.SetHandle(&typesmocks.MockQueueItemHandle{})
+							go item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
 							return nil
 						},
 					}
@@ -412,13 +508,16 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 					newMockShard("shard-B").withByteSize(100).build(),
 				},
 				setupProcessors: func(t *testing.T, h *testHarness) {
+					// Both processors reject the initial non-blocking Submit.
 					h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
 						SubmitFunc: func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
 					}
+					// Shard-B is the least loaded, so it should receive the blocking fallback (SubmitOrBlock).
 					h.mockProcessorFactory.processors["shard-B"] = &mockShardProcessor{
 						SubmitFunc: func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
 						SubmitOrBlockFunc: func(_ context.Context, item *internal.FlowItem) error {
-							go item.Finalize(types.QueueOutcomeDispatched, nil)
+							// The blocking call succeeds.
+							go item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
 							return nil
 						},
 					}
@@ -426,24 +525,78 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 				expectedOutcome: types.QueueOutcomeDispatched,
 			},
 			{
-				name:   "Rejects_AfterBlocking_WithAllProcessorsRemainingBusy",
+				// Validates the scenario where the request's TTL expires while the controller is blocked waiting for capacity.
+				// NOTE: This relies on real time passing, as context.WithDeadline timers cannot be controlled by FakeClock.
+				name:       "Rejects_AfterBlocking_WhenTTL_Expires",
+				shards:     []contracts.RegistryShard{newMockShard("shard-A").build()},
+				requestTTL: 50 * time.Millisecond, // Short TTL to keep the test fast.
+				setupProcessors: func(t *testing.T, h *testHarness) {
+					h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
+						// Reject the non-blocking attempt.
+						SubmitFunc: func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
+						// Block the fallback attempt until the context (carrying the TTL deadline) expires.
+						SubmitOrBlockFunc: func(ctx context.Context, _ *internal.FlowItem) error {
+							<-ctx.Done()
+							return ctx.Err()
+						},
+					}
+				},
+				// No runActions needed; we rely on the real-time timer to expire.
+				// When the blocking call fails due to context expiry, the outcome is RejectedOther.
+				expectedOutcome: types.QueueOutcomeRejectedOther,
+				expectErr:       true,
+				// The error must reflect the specific cause of the context cancellation (ErrTTLExpired).
+				expectErrIs: types.ErrTTLExpired,
+			},
+			{
+				name:   "Rejects_OnProcessorShutdownDuringSubmit",
 				shards: []contracts.RegistryShard{newMockShard("shard-A").build()},
 				setupProcessors: func(t *testing.T, h *testHarness) {
 					h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
-						SubmitFunc:        func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
-						SubmitOrBlockFunc: func(_ context.Context, _ *internal.FlowItem) error { return context.DeadlineExceeded },
+						// Simulate the processor shutting down during the non-blocking handoff.
+						SubmitFunc: func(_ *internal.FlowItem) error { return types.ErrFlowControllerNotRunning },
+						SubmitOrBlockFunc: func(_ context.Context, _ *internal.FlowItem) error {
+							return types.ErrFlowControllerNotRunning
+						},
 					}
 				},
-				expectedOutcome: types.QueueOutcomeRejectedCapacity,
+				expectedOutcome: types.QueueOutcomeRejectedOther,
 				expectErr:       true,
+				expectErrIs:     types.ErrFlowControllerNotRunning,
+			},
+			{
+				name:   "Rejects_OnProcessorShutdownDuringSubmitOrBlock",
+				shards: []contracts.RegistryShard{newMockShard("shard-A").build()},
+				setupProcessors: func(t *testing.T, h *testHarness) {
+					h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
+						SubmitFunc: func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
+						// Simulate the processor shutting down during the blocking handoff.
+						SubmitOrBlockFunc: func(_ context.Context, _ *internal.FlowItem) error {
+							return types.ErrFlowControllerNotRunning
+						},
+					}
+				},
+				expectedOutcome: types.QueueOutcomeRejectedOther,
+				expectErr:       true,
+				expectErrIs:     types.ErrFlowControllerNotRunning,
 			},
 		}
 
 		for _, tc := range testCases {
 			t.Run(tc.name, func(t *testing.T) {
 				t.Parallel()
+
+				// Arrange
 				mockRegistry := &mockRegistryClient{}
-				h := newUnitHarness(t, t.Context(), Config{}, mockRegistry)
+
+				// Configure the harness with the appropriate TTL.
+				harnessConfig := Config{DefaultRequestTTL: defaultTestTTL}
+				if tc.requestTTL > 0 {
+					harnessConfig.DefaultRequestTTL = tc.requestTTL
+				}
+				h := newUnitHarness(t, t.Context(), harnessConfig, mockRegistry)
+
+				// Configure the registry to return the specified shards.
 				mockRegistry.WithConnectionFunc = func(
 					_ types.FlowKey,
 					fn func(conn contracts.ActiveFlowConnection) error,
@@ -451,11 +604,33 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 					return fn(&mockActiveFlowConnection{ActiveShardsV: tc.shards})
 				}
 				tc.setupProcessors(t, h)
-				outcome, err := h.fc.EnqueueAndWait(newTestRequest(context.Background(), defaultFlowKey))
+
+				// Act
+				var outcome types.QueueOutcome
+				var err error
+
+				startTime := time.Now() // Capture real start time for duration checks.
+				// Use a background context for the parent; the request lifecycle is governed by the config/derived context.
+				outcome, err = h.fc.EnqueueAndWait(context.Background(), newTestRequest(defaultFlowKey))
+
+				// Assert
 				if tc.expectErr {
-					require.Error(t, err, "expected an error but got nil")
+					require.Error(t, err, "expected an error during EnqueueAndWait but got nil")
+					assert.ErrorIs(t, err, tc.expectErrIs, "error should wrap the expected underlying cause")
+					// All failures during the distribution phase (capacity, timeout, shutdown) should result in a rejection.
+					assert.ErrorIs(t, err, types.ErrRejected, "rejection errors must wrap types.ErrRejected")
+
+					// Specific assertion for real-time TTL tests.
+					if errors.Is(tc.expectErrIs, types.ErrTTLExpired) {
+						duration := time.Since(startTime)
+						// Ensure the test didn't return instantly. Use a tolerance for CI environments.
+						// This validates that the real-time wait actually occurred.
+						assert.GreaterOrEqual(t, duration, tc.requestTTL-30*time.Millisecond,
+							"EnqueueAndWait returned faster than the TTL allows, indicating the timer did not function correctly")
+					}
+
 				} else {
-					require.NoError(t, err, "expected no error but got: %v", err)
+					require.NoError(t, err, "expected no error during EnqueueAndWait but got: %v", err)
 				}
 				assert.Equal(t, tc.expectedOutcome, outcome, "outcome did not match expected value")
 			})
@@ -465,6 +640,8 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 	t.Run("Retry", func(t *testing.T) {
 		t.Parallel()
 
+		// This test specifically validates the behavior when the request context is cancelled externally while the
+		// controller is blocked in the SubmitOrBlock phase.
 		t.Run("Rejects_OnRequestContextCancelledWhileBlocking", func(t *testing.T) {
 			t.Parallel()
 			mockRegistry := &mockRegistryClient{
@@ -477,22 +654,34 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 					})
 				},
 			}
-			h := newUnitHarness(t, t.Context(), Config{}, mockRegistry)
+			// Use a long TTL to ensure the failure is due to cancellation, not timeout.
+			h := newUnitHarness(t, t.Context(), Config{DefaultRequestTTL: 10 * time.Second}, mockRegistry)
 			h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
-				SubmitFunc:        func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
-				SubmitOrBlockFunc: func(ctx context.Context, _ *internal.FlowItem) error { <-ctx.Done(); return ctx.Err() },
+				// Reject non-blocking attempt.
+				SubmitFunc: func(_ *internal.FlowItem) error { return internal.ErrProcessorBusy },
+				// Block the fallback attempt until the context is cancelled.
+				SubmitOrBlockFunc: func(ctx context.Context, _ *internal.FlowItem) error {
+					<-ctx.Done()
+					return ctx.Err()
+				},
 			}
+
+			// Create a cancellable context for the request.
 			reqCtx, cancelReq := context.WithCancel(context.Background())
-			go func() { time.Sleep(50 * time.Millisecond); cancelReq() }()
+			// Cancel the request shortly after starting the operation.
+			// We use real time sleep here as we are testing external cancellation signals interacting with the context.
+			go func() { time.Sleep(10 * time.Millisecond); cancelReq() }()
 
-			outcome, err := h.fc.EnqueueAndWait(newTestRequest(reqCtx, defaultFlowKey))
+			outcome, err := h.fc.EnqueueAndWait(reqCtx, newTestRequest(defaultFlowKey))
 
 			require.Error(t, err, "EnqueueAndWait must fail when context is cancelled during a blocking submit")
 			assert.ErrorIs(t, err, types.ErrRejected, "error should wrap ErrRejected")
-			assert.ErrorIs(t, err, context.Canceled, "error should wrap the underlying ctx.Err()")
-			assert.Equal(t, types.QueueOutcomeRejectedCapacity, outcome, "outcome should be QueueOutcomeRejectedCapacity")
+			assert.ErrorIs(t, err, context.Canceled, "error should wrap the underlying ctx.Err() (context.Canceled)")
+			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome,
+				"outcome should be QueueOutcomeRejectedOther when cancelled during distribution")
 		})
 
+		// This test validates the retry mechanism when a processor reports that its shard is draining.
 		t.Run("RetriesAndSucceeds_OnProcessorReportsShardDraining", func(t *testing.T) {
 			t.Parallel()
 			var callCount atomic.Int32
@@ -504,106 +693,278 @@ func TestFlowController_EnqueueAndWait(t *testing.T) {
 					attempt := callCount.Add(1)
 					shardA := newMockShard("shard-A").withByteSize(100).build()
 					shardB := newMockShard("shard-B").withByteSize(1000).build()
+
 					if attempt == 1 {
+						// Attempt 1: Shard A is the least loaded and is selected.
 						return fn(&mockActiveFlowConnection{ActiveShardsV: []contracts.RegistryShard{shardA, shardB}})
 					}
+					// Attempt 2 (Retry): Assume Shard A is now draining and removed from the active set by the registry.
 					return fn(&mockActiveFlowConnection{ActiveShardsV: []contracts.RegistryShard{shardB}})
 				},
 			}
-			h := newUnitHarness(t, t.Context(), Config{}, mockRegistry)
+			// Use a long TTL to ensure retries don't time out.
+			h := newUnitHarness(t, t.Context(), Config{DefaultRequestTTL: 10 * time.Second}, mockRegistry)
+
+			// Configure Shard A's processor to reject the request due to draining.
 			h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
 				SubmitFunc: func(item *internal.FlowItem) error {
-					go item.Finalize(types.QueueOutcomeRejectedOther, contracts.ErrShardDraining)
+					// The processor accepts the item but then asynchronously finalizes it with ErrShardDraining.
+					item.SetHandle(&typesmocks.MockQueueItemHandle{})
+					go item.FinalizeWithOutcome(types.QueueOutcomeRejectedOther, contracts.ErrShardDraining)
 					return nil
 				},
 			}
+			// Configure Shard B's processor to successfully dispatch the request on the retry.
 			h.mockProcessorFactory.processors["shard-B"] = &mockShardProcessor{
 				SubmitFunc: func(item *internal.FlowItem) error {
-					go item.Finalize(types.QueueOutcomeDispatched, nil)
+					go item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
 					return nil
 				},
 			}
 
-			outcome, err := h.fc.EnqueueAndWait(newTestRequest(context.Background(), defaultFlowKey))
+			// Act
+			outcome, err := h.fc.EnqueueAndWait(context.Background(), newTestRequest(defaultFlowKey))
+
+			// Assert
 			require.NoError(t, err, "EnqueueAndWait must succeed after retrying on a healthy shard")
 			assert.Equal(t, types.QueueOutcomeDispatched, outcome, "outcome should be QueueOutcomeDispatched")
 			assert.Equal(t, int32(2), callCount.Load(), "registry must be consulted for Active shards on each retry attempt")
 		})
 	})
+
+	// Lifecycle covers the post-distribution phase, focusing on how the controller handles context cancellation and TTL
+	// expiry while the request is buffered or queued by the processor (Asynchronous Finalization).
+	t.Run("Lifecycle", func(t *testing.T) {
+		t.Parallel()
+
+		// Validates that the controller correctly initiates asynchronous finalization when the request context is cancelled
+		// after ownership has been transferred to the processor.
+		t.Run("OnReqCtxCancelledAfterDistribution", func(t *testing.T) {
+			t.Parallel()
+			// Use a long TTL to ensure the failure is due to cancellation.
+			h := newUnitHarness(t, t.Context(), Config{DefaultRequestTTL: 10 * time.Second}, nil)
+
+			shardA := newMockShard("shard-A").build()
+			h.mockRegistry.WithConnectionFunc = func(_ types.FlowKey, fn func(_ contracts.ActiveFlowConnection) error) error {
+				return fn(&mockActiveFlowConnection{ActiveShardsV: []contracts.RegistryShard{shardA}})
+			}
+
+			// Channel for synchronization.
+			itemSubmitted := make(chan *internal.FlowItem, 1)
+
+			// Configure the processor to accept the item but never finalize it, simulating a queued request.
+			h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
+				SubmitFunc: func(item *internal.FlowItem) error {
+					item.SetHandle(&typesmocks.MockQueueItemHandle{})
+					itemSubmitted <- item
+					return nil
+				},
+			}
+
+			reqCtx, cancelReq := context.WithCancel(context.Background())
+			req := newTestRequest(defaultFlowKey)
+
+			var outcome types.QueueOutcome
+			var err error
+			done := make(chan struct{})
+			go func() {
+				outcome, err = h.fc.EnqueueAndWait(reqCtx, req)
+				close(done)
+			}()
+
+			// 1. Wait for the item to be successfully distributed.
+			var item *internal.FlowItem
+			select {
+			case item = <-itemSubmitted:
+				// Success. Ownership has transferred. EnqueueAndWait is now in the select loop.
+			case <-time.After(1 * time.Second):
+				t.Fatal("timed out waiting for item to be submitted to the processor")
+			}
+
+			// 2. Cancel the request context.
+			cancelReq()
+
+			// 3. Wait for EnqueueAndWait to return.
+			select {
+			case <-done:
+				// Success. The controller detected the cancellation and unblocked the caller.
+			case <-time.After(1 * time.Second):
+				t.Fatal("timed out waiting for EnqueueAndWait to return after cancellation")
+			}
+
+			// 4. Assertions for EnqueueAndWait's return values.
+			require.Error(t, err, "EnqueueAndWait should return an error when the request is cancelled post-distribution")
+			// The outcome should be Evicted (as the handle was set).
+			assert.ErrorIs(t, err, types.ErrEvicted, "error should wrap ErrEvicted")
+			// The underlying cause must be propagated.
+			assert.ErrorIs(t, err, types.ErrContextCancelled, "error should wrap ErrContextCancelled")
+			assert.Equal(t, types.QueueOutcomeEvictedContextCancelled, outcome, "outcome should be EvictedContextCancelled")
+
+			// 5. Assert that the FlowItem itself was indeed finalized by the controller.
+			finalState := item.FinalState()
+			require.NotNil(t, finalState, "Item should have been finalized asynchronously by the controller")
+			assert.Equal(t, types.QueueOutcomeEvictedContextCancelled, finalState.Outcome,
+				"Item's internal outcome must match the returned outcome")
+		})
+
+		// Validates the asynchronous finalization path due to TTL expiry.
+		// Note: This relies on real time passing, as context.WithDeadline timers cannot be controlled by FakeClock.
+		t.Run("OnReqCtxTimeoutAfterDistribution", func(t *testing.T) {
+			t.Parallel()
+			// Configure a short TTL to keep the test reasonably fast.
+			const requestTTL = 50 * time.Millisecond
+			h := newUnitHarness(t, t.Context(), Config{DefaultRequestTTL: requestTTL}, nil)
+
+			shardA := newMockShard("shard-A").build()
+			h.mockRegistry.WithConnectionFunc = func(_ types.FlowKey, fn func(_ contracts.ActiveFlowConnection) error) error {
+				return fn(&mockActiveFlowConnection{ActiveShardsV: []contracts.RegistryShard{shardA}})
+			}
+
+			itemSubmitted := make(chan *internal.FlowItem, 1)
+
+			// Configure the processor to accept the item but never finalize it.
+			h.mockProcessorFactory.processors["shard-A"] = &mockShardProcessor{
+				SubmitFunc: func(item *internal.FlowItem) error {
+					item.SetHandle(&typesmocks.MockQueueItemHandle{})
+					itemSubmitted <- item
+					return nil
+				},
+			}
+
+			req := newTestRequest(defaultFlowKey)
+			// Use a context for the call itself that won't time out independently.
+			enqueueCtx, enqueueCancel := context.WithTimeout(context.Background(), 2*time.Second)
+			defer enqueueCancel()
+
+			var outcome types.QueueOutcome
+			var err error
+			done := make(chan struct{})
+
+			startTime := time.Now() // Capture start time to validate duration.
+			go func() {
+				outcome, err = h.fc.EnqueueAndWait(enqueueCtx, req)
+				close(done)
+			}()
+
+			// 1. Wait for the item to be submitted.
+			var item *internal.FlowItem
+			select {
+			case item = <-itemSubmitted:
+			case <-time.After(1 * time.Second):
+				t.Fatal("timed out waiting for item to be submitted to the processor")
+			}
+
+			// 2.Wait for the TTL to expire (Real time). We do NOT call Step().
+			// Wait for EnqueueAndWait to return due to the TTL expiry.
+			select {
+			case <-done:
+				// Success. Now validate that enough time actually passed.
+				duration := time.Since(startTime)
+				assert.GreaterOrEqual(t, duration, requestTTL-30*time.Millisecond, // tolerance for CI environments
+					"EnqueueAndWait returned faster than the TTL allows, indicating the timer did not function correctly")
+			case <-time.After(1 * time.Second):
+				t.Fatal("timed out waiting for EnqueueAndWait to return after TTL expiry")
+			}
+
+			// 4. Assertions for EnqueueAndWait's return values.
+			require.Error(t, err, "EnqueueAndWait should return an error when TTL expires post-distribution")
+			assert.ErrorIs(t, err, types.ErrEvicted, "error should wrap ErrEvicted")
+			assert.ErrorIs(t, err, types.ErrTTLExpired, "error should wrap the underlying cause (types.ErrTTLExpired)")
+			assert.Equal(t, types.QueueOutcomeEvictedTTL, outcome, "outcome should be EvictedTTL")
+
+			// 5. Assert FlowItem final state.
+			finalState := item.FinalState()
+			require.NotNil(t, finalState, "Item should have been finalized asynchronously by the controller")
+			assert.Equal(t, types.QueueOutcomeEvictedTTL, finalState.Outcome,
+				"Item's internal outcome must match the returned outcome")
+		})
+	})
 }
 
-func TestFlowController_Lifecycle(t *testing.T) {
+// TestFlowController_WorkerManagement covers the lifecycle of the shard processors (workers), including startup,
+// reconciliation (garbage collection), and shutdown.
+func TestFlowController_WorkerManagement(t *testing.T) {
 	t.Parallel()
 
+	// Reconciliation validates that the controller correctly identifies and shuts down workers whose shards no longer
+	// exist in the registry.
 	t.Run("Reconciliation", func(t *testing.T) {
 		t.Parallel()
 
+		// Setup: A registry that initially knows about "shard-A" and "stale-shard", but later only reports "shard-A".
 		mockRegistry := &mockRegistryClient{
-			// Configure the mock registry to report the new state without the stale shard.
 			ShardStatsFunc: func() []contracts.ShardStats {
+				// The current state of the world according to the registry.
 				return []contracts.ShardStats{{ID: "shard-A"}}
 			}}
 		h := newUnitHarness(t, t.Context(), Config{}, mockRegistry)
 
-		// Pre-populate the controller with initial workers.
+		// Pre-populate the controller with initial workers, simulating a previous state.
 		initialShards := []string{"shard-A", "stale-shard"}
 		for _, shardID := range initialShards {
 			currentShardID := shardID
+			// Initialize the processor mocks with the channel needed to synchronize startup.
 			h.mockProcessorFactory.processors[currentShardID] = &mockShardProcessor{runStarted: make(chan struct{})}
 			shard := &mocks.MockRegistryShard{IDFunc: func() string { return currentShardID }}
+			// Start the worker using the internal mechanism.
 			h.fc.getOrStartWorker(shard)
 		}
 		require.Len(t, h.mockProcessorFactory.processors, 2, "pre-condition: initial workers not set up correctly")
 
-		// Wait for all workers to have started and set their contexts before proceeding with the test.
+		// Wait for all worker goroutines to have started and captured their contexts.
 		for id, p := range h.mockProcessorFactory.processors {
 			proc := p
 			select {
 			case <-proc.runStarted:
-				// Success
+				// Worker is running.
 			case <-time.After(2 * time.Second):
 				t.Fatalf("timed out waiting for worker %s to start", id)
 			}
 		}
 
-		// Manually trigger the reconciliation loop logic.
+		// Act: Manually trigger the reconciliation logic.
 		h.fc.reconcileProcessors()
 
 		t.Run("StaleWorkerIsCancelled", func(t *testing.T) {
 			staleProc := h.mockProcessorFactory.processors["stale-shard"]
 			require.NotNil(t, staleProc.Context(), "precondition: stale processor context should have been captured")
+			// The context of the removed worker must be cancelled to signal shutdown.
 			select {
 			case <-staleProc.Context().Done():
-				// Success
+				// Success: Context was cancelled.
 			case <-time.After(100 * time.Millisecond):
-				t.Error("context of removed worker must be cancelled")
+				t.Error("context of the stale worker was not cancelled during reconciliation")
 			}
 		})
 
 		t.Run("ActiveWorkerIsNotCancelled", func(t *testing.T) {
 			activeProc := h.mockProcessorFactory.processors["shard-A"]
 			require.NotNil(t, activeProc.Context(), "precondition: active processor context should have been captured")
+			// The context of an active worker must remain open.
 			select {
 			case <-activeProc.Context().Done():
-				t.Error("context of remaining worker must not be cancelled")
+				t.Error("context of the active worker was incorrectly cancelled during reconciliation")
 			default:
-				// Success
+				// Success: Context is still active.
 			}
 		})
 
 		t.Run("WorkerMapIsUpdated", func(t *testing.T) {
+			// The stale worker must be removed from the controller's concurrent map.
 			_, ok := h.fc.workers.Load("stale-shard")
-			assert.False(t, ok, "stale worker must be removed from the controller's map")
+			assert.False(t, ok, "stale worker must be deleted from the controller's map")
 			_, ok = h.fc.workers.Load("shard-A")
 			assert.True(t, ok, "active worker must remain in the controller's map")
 		})
 	})
 
+	// Validates that the reconciliation loop runs periodically based on the configured interval.
 	t.Run("Reconciliation_IsTriggeredByTicker", func(t *testing.T) {
 		t.Parallel()
-		reconciliationInterval := 10 * time.Second
+		const reconciliationInterval = 10 * time.Second
 		mockRegistry := &mockRegistryClient{}
 
+		// Count the number of times the reconciliation logic (which calls ShardStats) runs.
 		var reconcileCount atomic.Int32
 		mockRegistry.ShardStatsFunc = func() []contracts.ShardStats {
 			reconcileCount.Add(1)
@@ -611,45 +972,55 @@ func TestFlowController_Lifecycle(t *testing.T) {
 		}
 
 		h := newUnitHarness(t, t.Context(), Config{ProcessorReconciliationInterval: reconciliationInterval}, mockRegistry)
+		// Ensure we are using the FakeClock specifically for this test, as we need Step/HasWaiters.
+		require.NotNil(t, h.mockClock, "This test requires the harness to be using FakeClock")
 
 		// Wait for the reconciliation loop to start and create the ticker.
-		// This prevents a race where the clock is stepped before the ticker is registered.
-		require.Eventually(t, h.mockClock.HasWaiters, time.Second, 10*time.Millisecond, "ticker was not created")
+		// This prevents a race where the clock is stepped before the ticker is registered with the FakeClock.
+		require.Eventually(t, h.mockClock.HasWaiters, time.Second, 10*time.Millisecond,
+			"reconciliation ticker was not created")
 
-		// Advance the clock to trigger the next reconciliation.
+		// Advance the clock to trigger the first reconciliation.
 		h.mockClock.Step(reconciliationInterval)
 
 		assert.Eventually(t, func() bool {
 			return reconcileCount.Load() == 1
-		}, time.Second, 10*time.Millisecond, "reconciliation was not triggered by the ticker")
+		}, time.Second, 10*time.Millisecond, "reconciliation was not triggered by the first ticker event")
 
 		// Advance the clock again to ensure it continues to fire.
 		h.mockClock.Step(reconciliationInterval)
 		assert.Eventually(t, func() bool {
 			return reconcileCount.Load() == 2
-		}, time.Second, 10*time.Millisecond, "reconciliation did not fire on the second tick")
+		}, time.Second, 10*time.Millisecond, "reconciliation did not fire on the second ticker event")
 	})
 
+	// Validates the atomicity of worker creation and ensures resource cleanup for the loser of the race.
 	t.Run("WorkerCreationRace", func(t *testing.T) {
 		t.Parallel()
 
-		// This test requires manual control over the shard processor factory to deterministically create a race.
+		// This test orchestrates a deterministic race condition.
 		factoryEntered := make(chan *mockShardProcessor, 2)
 		continueFactory := make(chan struct{})
+		// Map to store the construction context for each processor instance, allowing us to verify cleanup.
+		constructionContexts := sync.Map{}
 
-		h := newUnitHarness(t, t.Context(), Config{}, &mockRegistryClient{})
+		h := newUnitHarness(t, t.Context(), Config{}, nil)
+
+		// Inject a custom factory to control the timing of worker creation.
 		h.fc.shardProcessorFactory = func(
+			ctx context.Context, // The context created by getOrStartWorker for the potential new processor.
 			shard contracts.RegistryShard,
 			_ contracts.SaturationDetector,
-			_ clock.Clock,
+			_ clock.WithTicker,
 			_ time.Duration,
 			_ int,
 			_ logr.Logger,
 		) shardProcessor {
-			// This factory function will be called by `startNewWorker`.
-			// We use channels to pause execution here, allowing two goroutines to enter this function before one "wins"
-			// the `LoadOrStore` race.
+			// This function is called by getOrStartWorker before the LoadOrStore check.
 			proc := &mockShardProcessor{runStarted: make(chan struct{})}
+			constructionContexts.Store(proc, ctx) // Capture the construction context.
+
+			// Signal entry and then block, allowing another goroutine to enter.
 			factoryEntered <- proc
 			<-continueFactory
 			return proc
@@ -669,18 +1040,17 @@ func TestFlowController_Lifecycle(t *testing.T) {
 			h.fc.getOrStartWorker(shard)
 		}()
 
-		// Wait for both goroutines to have entered the factory and created a processor.
-		// This confirms they both missed the initial `workers.Load` check.
+		// 1. Wait for both goroutines to enter the factory and create their respective processor instances.
 		proc1 := <-factoryEntered
 		proc2 := <-factoryEntered
 
-		// Unblock both goroutines, allowing them to race to `workers.LoadOrStore`.
+		// 2. Unblock both goroutines, allowing them to race to workers.LoadOrStore.
 		close(continueFactory)
 		wg.Wait()
 
-		// One processor "won" and was stored, the other "lost" and should have been cancelled.
+		// 3. Identify the winner and the loser.
 		actual, ok := h.fc.workers.Load("race-shard")
-		require.True(t, ok, "a worker should have been stored in the map")
+		require.True(t, ok, "a worker must have been successfully stored in the map")
 
 		storedWorker := actual.(*managedWorker)
 		winnerProc := storedWorker.processor.(*mockShardProcessor)
@@ -692,16 +1062,17 @@ func TestFlowController_Lifecycle(t *testing.T) {
 			loserProc = proc1
 		}
 
-		// Wait for the `Run` method to be called on the winning processor to ensure its context is available.
+		// 4. Validate the state of the winning processor.
+		// Wait for the Run method to be called on the winner (only the winner should start).
 		select {
 		case <-winnerProc.runStarted:
-		// Success.
+			// Success.
 		case <-time.After(1 * time.Second):
-			t.Fatal("timed out waiting for winning worker to start")
+			t.Fatal("timed out waiting for the winning worker's Run method to be called")
 		}
 
 		// The winning processor's context must remain active.
-		require.NotNil(t, winnerProc.Context(), "winner's context should not be nil")
+		require.NotNil(t, winnerProc.Context(), "winner's context should not be nil (Run was called)")
 		select {
 		case <-winnerProc.Context().Done():
 			t.Error("context of the winning worker should not be cancelled")
@@ -709,49 +1080,61 @@ func TestFlowController_Lifecycle(t *testing.T) {
 			// Success
 		}
 
-		// The losing processor's `Run` method must not be called, and its context should be nil.
+		// 5. Validate the state of the losing processor and resource cleanup.
+		// The losing processor's Run method must NOT be called.
 		select {
 		case <-loserProc.runStarted:
-			t.Error("Run was called on the losing worker, but it should not have been")
+			t.Error("Run was incorrectly called on the losing worker")
 		default:
 			// Success
 		}
-		assert.Nil(t, loserProc.Context(), "loser's context should be nil as Run is never called")
+
+		// Verify the context created for the loser during construction was cancelled by getOrStartWorker.
+		loserCtxRaw, ok := constructionContexts.Load(loserProc)
+		require.True(t, ok, "loser processor construction context should have been captured")
+		loserCtx := loserCtxRaw.(context.Context)
+
+		select {
+		case <-loserCtx.Done():
+			// Success: Context was cancelled, preventing resource leaks.
+		case <-time.After(100 * time.Millisecond):
+			t.Error("context of the losing worker was not cancelled, this will leak resources")
+		}
 	})
 }
 
-func TestFlowController_Concurrency(t *testing.T) {
-	const (
-		numShards     = 4
-		numGoroutines = 50
-		numRequests   = 200
-	)
-
-	// Set up a realistic registry that vends real components to the processor.
+// Helper function to create a realistic mock registry environment for integration/concurrency tests.
+func setupRegistryForConcurrency(t *testing.T, numShards int, flowKey types.FlowKey) *mockRegistryClient {
+	t.Helper()
 	mockRegistry := &mockRegistryClient{}
 	shards := make([]contracts.RegistryShard, numShards)
-	queues := make(map[string]contracts.ManagedQueue)
+
+	// Configure the shards and their dependencies required by the real ShardProcessor implementation.
 	for i := range numShards {
+		// Capture loop variables for closures.
 		shardID := fmt.Sprintf("shard-%d", i)
-		queues[shardID] = &mocks.MockManagedQueue{FlowKeyV: defaultFlowKey} // Use the high-fidelity mock queue.
+		// Use high-fidelity mock queues (MockManagedQueue) that implement the necessary interfaces and synchronization.
+		currentQueue := &mocks.MockManagedQueue{FlowKeyV: flowKey}
+
 		shards[i] = &mocks.MockRegistryShard{
 			IDFunc: func() string { return shardID },
 			ManagedQueueFunc: func(_ types.FlowKey) (contracts.ManagedQueue, error) {
-				return queues[shardID], nil
+				return currentQueue, nil
 			},
-			AllOrderedPriorityLevelsFunc: func() []int { return []int{100} },
+			// Configuration required for ShardProcessor initialization and dispatch logic.
+			AllOrderedPriorityLevelsFunc: func() []int { return []int{flowKey.Priority} },
 			PriorityBandAccessorFunc: func(priority int) (framework.PriorityBandAccessor, error) {
-				if priority == 100 {
+				if priority == flowKey.Priority {
 					return &frameworkmocks.MockPriorityBandAccessor{
-						PriorityNameV: "high",
-						PriorityV:     100,
+						PriorityV: priority,
 						IterateQueuesFunc: func(f func(framework.FlowQueueAccessor) bool) {
-							f(queues[shardID].FlowQueueAccessor())
+							f(currentQueue.FlowQueueAccessor())
 						},
 					}, nil
 				}
 				return nil, fmt.Errorf("unexpected priority %d", priority)
 			},
+			// Configure dispatch policies (FIFO).
 			IntraFlowDispatchPolicyFunc: func(_ types.FlowKey) (framework.IntraFlowDispatchPolicy, error) {
 				return &frameworkmocks.MockIntraFlowDispatchPolicy{
 					SelectItemFunc: func(qa framework.FlowQueueAccessor) (types.QueueItemAccessor, error) {
@@ -762,26 +1145,29 @@ func TestFlowController_Concurrency(t *testing.T) {
 			InterFlowDispatchPolicyFunc: func(_ int) (framework.InterFlowDispatchPolicy, error) {
 				return &frameworkmocks.MockInterFlowDispatchPolicy{
 					SelectQueueFunc: func(band framework.PriorityBandAccessor) (framework.FlowQueueAccessor, error) {
-						return queues[shardID].FlowQueueAccessor(), nil
+						return currentQueue.FlowQueueAccessor(), nil
 					},
 				}, nil
 			},
+			// Configure stats reporting based on the live state of the mock queues.
 			StatsFunc: func() contracts.ShardStats {
 				return contracts.ShardStats{
 					ID:            shardID,
-					TotalLen:      uint64(queues[shardID].Len()),
-					TotalByteSize: queues[shardID].ByteSize(),
+					TotalLen:      uint64(currentQueue.Len()),
+					TotalByteSize: currentQueue.ByteSize(),
 					PerPriorityBandStats: map[int]contracts.PriorityBandStats{
-						100: {
-							Len:           uint64(queues[shardID].Len()),
-							ByteSize:      queues[shardID].ByteSize(),
-							CapacityBytes: 1e9, // Effectively unlimited capacity
+						flowKey.Priority: {
+							Len:           uint64(currentQueue.Len()),
+							ByteSize:      currentQueue.ByteSize(),
+							CapacityBytes: 1e9, // Effectively unlimited capacity to ensure dispatch success.
 						},
 					},
 				}
 			},
 		}
 	}
+
+	// Configure the registry connection.
 	mockRegistry.WithConnectionFunc = func(_ types.FlowKey, fn func(conn contracts.ActiveFlowConnection) error) error {
 		return fn(&mockActiveFlowConnection{ActiveShardsV: shards})
 	}
@@ -792,25 +1178,121 @@ func TestFlowController_Concurrency(t *testing.T) {
 		}
 		return stats
 	}
+	return mockRegistry
+}
+
+// TestFlowController_Concurrency_Distribution performs an integration test under high contention, using real
+// ShardProcessors.
+// It validates the thread-safety of the distribution logic and the overall system throughput.
+func TestFlowController_Concurrency_Distribution(t *testing.T) {
+	const (
+		numShards     = 4
+		numGoroutines = 50
+		numRequests   = 200
+	)
+
+	// Arrange
+	mockRegistry := setupRegistryForConcurrency(t, numShards, defaultFlowKey)
+
+	// Initialize the integration harness with real ShardProcessors.
 	h := newIntegrationHarness(t, t.Context(), Config{
-		// Use a generous buffer to prevent flakes in the test due to transient queuing delays.
+		// Use a generous buffer to focus the test on distribution logic rather than backpressure.
 		EnqueueChannelBufferSize: numRequests,
-		DefaultRequestTTL:        1 * time.Second,
+		DefaultRequestTTL:        5 * time.Second,
 		ExpiryCleanupInterval:    100 * time.Millisecond,
 	}, mockRegistry)
 
+	// Act: Hammer the controller concurrently.
 	var wg sync.WaitGroup
 	wg.Add(numGoroutines)
 	outcomes := make(chan types.QueueOutcome, numRequests)
-	for range numGoroutines {
+
+	for i := range numGoroutines {
+		goroutineID := i
 		go func() {
 			defer wg.Done()
-			for range numRequests / numGoroutines {
-				req := newTestRequest(logr.NewContext(context.Background(), logr.Discard()), defaultFlowKey)
-				outcome, err := h.fc.EnqueueAndWait(req)
+			for j := range numRequests / numGoroutines {
+				req := newTestRequest(defaultFlowKey)
+				req.IDV = fmt.Sprintf("req-distrib-%d-%d", goroutineID, j)
+
+				// Use a reasonable timeout for the individual request context.
+				reqCtx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
+				defer cancel()
+
+				ctx := logr.NewContext(reqCtx, logr.Discard())
+				outcome, err := h.fc.EnqueueAndWait(ctx, req)
+				if err != nil {
+					// Use t.Errorf for concurrent tests to report failures without halting execution.
+					t.Errorf("EnqueueAndWait failed unexpectedly under load: %v", err)
+				}
+				outcomes <- outcome
+			}
+		}()
+	}
+
+	// Wait for all requests to complete.
+	wg.Wait()
+	close(outcomes)
+
+	// Assert: All requests should be successfully dispatched.
+	successCount := 0
+	for outcome := range outcomes {
+		if outcome == types.QueueOutcomeDispatched {
+			successCount++
+		}
+	}
+	require.Equal(t, numRequests, successCount,
+		"all concurrent requests must be dispatched successfully without errors or data races")
+}
+
+// TestFlowController_Concurrency_Backpressure specifically targets the blocking submission path (SubmitOrBlock) by
+// configuring the processors with zero buffer capacity.
+func TestFlowController_Concurrency_Backpressure(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping concurrency integration test in short mode.")
+	}
+	t.Parallel()
+
+	const (
+		numShards     = 2
+		numGoroutines = 20
+		// Fewer requests than the distribution test, as the blocking path is inherently slower.
+		numRequests = 40
+	)
+
+	// Arrange: Set up the registry environment.
+	mockRegistry := setupRegistryForConcurrency(t, numShards, defaultFlowKey)
+
+	// Use the integration harness with a configuration designed to induce backpressure.
+	h := newIntegrationHarness(t, t.Context(), Config{
+		// Zero buffer forces immediate use of SubmitOrBlock if the processor loop is busy.
+		EnqueueChannelBufferSize: 0,
+		// Generous TTL to ensure timeouts are not the cause of failure.
+		DefaultRequestTTL:     10 * time.Second,
+		ExpiryCleanupInterval: 100 * time.Millisecond,
+	}, mockRegistry)
+
+	// Act: Concurrently submit requests.
+	var wg sync.WaitGroup
+	wg.Add(numGoroutines)
+	outcomes := make(chan types.QueueOutcome, numRequests)
+
+	for i := range numGoroutines {
+		goroutineID := i
+		go func() {
+			defer wg.Done()
+			for j := range numRequests / numGoroutines {
+				req := newTestRequest(defaultFlowKey)
+				req.IDV = fmt.Sprintf("req-backpressure-%d-%d", goroutineID, j)
+
+				// Use a reasonable timeout for the individual request context to ensure the test finishes promptly if a
+				// deadlock occurs.
+				reqCtx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
+				defer cancel()
+
+				outcome, err := h.fc.EnqueueAndWait(logr.NewContext(reqCtx, logr.Discard()), req)
 				if err != nil {
-					// Use `t.Errorf` for concurrent tests to avoid halting execution on a single failure.
-					t.Errorf("EnqueueAndWait failed unexpectedly: %v", err)
+					t.Errorf("EnqueueAndWait failed unexpectedly under backpressure for request %s: %v", req.ID(), err)
 				}
 				outcomes <- outcome
 			}
@@ -819,11 +1301,13 @@ func TestFlowController_Concurrency(t *testing.T) {
 	wg.Wait()
 	close(outcomes)
 
+	// Assert: Verify successful dispatch despite high contention and zero buffer.
 	successCount := 0
 	for outcome := range outcomes {
 		if outcome == types.QueueOutcomeDispatched {
 			successCount++
 		}
 	}
-	require.Equal(t, numRequests, successCount, "all concurrent requests should be dispatched successfully")
+	require.Equal(t, numRequests, successCount,
+		"all concurrent requests should be dispatched successfully even under high contention and zero buffer capacity")
 }
diff --git a/pkg/epp/flowcontrol/controller/internal/item_test.go b/pkg/epp/flowcontrol/controller/internal/item_test.go
index 1f713e9..9b7b627 100644
--- a/pkg/epp/flowcontrol/controller/internal/item_test.go
+++ b/pkg/epp/flowcontrol/controller/internal/item_test.go
@@ -31,12 +31,16 @@ import (
 
 func TestFlowItem_New(t *testing.T) {
 	t.Parallel()
-	req := typesmocks.NewMockFlowControlRequest(100, "req-1", types.FlowKey{}, context.Background())
+	req := typesmocks.NewMockFlowControlRequest(100, "req-1", types.FlowKey{})
 
-	item := NewItem(req, time.Minute, time.Now())
+	enqueueTime := time.Now()
+	item := NewItem(req, time.Minute, enqueueTime)
 
 	require.NotNil(t, item, "NewItem should not return a nil item")
-	assert.False(t, item.isFinalized(), "a new item must not be in a finalized state")
+	assert.Equal(t, enqueueTime, item.EnqueueTime(), "EnqueueTime should be populated")
+	assert.Equal(t, time.Minute, item.EffectiveTTL(), "EffectiveTTL should be populated")
+	assert.Same(t, req, item.OriginalRequest(), "OriginalRequest should be populated")
+	assert.Nil(t, item.FinalState(), "a new item must not have a final state")
 	select {
 	case <-item.Done():
 		t.Fatal("Done() channel for a new item must block, but it was closed")
@@ -55,21 +59,178 @@ func TestFlowItem_Handle(t *testing.T) {
 
 func TestFlowItem_Finalize_Idempotency(t *testing.T) {
 	t.Parallel()
-	req := typesmocks.NewMockFlowControlRequest(100, "req-1", types.FlowKey{}, context.Background())
-	item := NewItem(req, time.Minute, time.Now())
-	expectedErr := errors.New("first-error")
+	now := time.Now()
+	req := typesmocks.NewMockFlowControlRequest(100, "req-1", types.FlowKey{})
 
-	item.Finalize(types.QueueOutcomeEvictedTTL, expectedErr)
-	item.Finalize(types.QueueOutcomeDispatched, nil) // Should take no effect
+	testCases := []struct {
+		name            string
+		firstCall       func(item *FlowItem)
+		secondCall      func(item *FlowItem)
+		expectedOutcome types.QueueOutcome
+		expectedErrIs   error
+	}{
+		{
+			name: "Finalize then Finalize",
+			firstCall: func(item *FlowItem) {
+				item.Finalize(types.ErrTTLExpired)
+			},
+			secondCall: func(item *FlowItem) {
+				item.Finalize(context.Canceled)
+			},
+			expectedOutcome: types.QueueOutcomeRejectedOther,
+			expectedErrIs:   types.ErrTTLExpired,
+		},
+		{
+			name: "Finalize then FinalizeWithOutcome",
+			firstCall: func(item *FlowItem) {
+				item.Finalize(types.ErrTTLExpired)
+			},
+			secondCall: func(item *FlowItem) {
+				item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
+			},
+			expectedOutcome: types.QueueOutcomeRejectedOther,
+			expectedErrIs:   types.ErrTTLExpired,
+		},
+		{
+			name: "FinalizeWithOutcome then FinalizeWithOutcome",
+			firstCall: func(item *FlowItem) {
+				item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
+			},
+			secondCall: func(item *FlowItem) {
+				item.FinalizeWithOutcome(types.QueueOutcomeRejectedCapacity, errors.New("rejected"))
+			},
+			expectedOutcome: types.QueueOutcomeDispatched,
+			expectedErrIs:   nil,
+		},
+		{
+			name: "FinalizeWithOutcome then Finalize",
+			firstCall: func(item *FlowItem) {
+				item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
+			},
+			secondCall: func(item *FlowItem) {
+				item.Finalize(types.ErrTTLExpired)
+			},
+			expectedOutcome: types.QueueOutcomeDispatched,
+			expectedErrIs:   nil,
+		},
+	}
 
-	assert.True(t, item.isFinalized(), "item must be in a finalized state after a call to finalize()")
-	select {
-	case finalState, ok := <-item.Done():
-		require.True(t, ok, "Done() channel should be readable with a value, not just closed")
-		assert.Equal(t, types.QueueOutcomeEvictedTTL, finalState.Outcome,
-			"the outcome from Done() must match the first finalized outcome")
-		assert.Equal(t, expectedErr, finalState.Err, "the error from Done() must match the first finalized error")
-	case <-time.After(50 * time.Millisecond):
-		t.Fatal("Done() channel must not block after finalization")
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			item := NewItem(req, time.Minute, now)
+
+			// First call
+			tc.firstCall(item)
+
+			// Second call
+			tc.secondCall(item)
+
+			// Check FinalState()
+			finalState := item.FinalState()
+			require.NotNil(t, finalState, "FinalState should not be nil")
+			assert.Equal(t, tc.expectedOutcome, finalState.Outcome, "Outcome should match the first call")
+			if tc.expectedErrIs != nil {
+				assert.ErrorIs(t, finalState.Err, tc.expectedErrIs, "Error should match the first call")
+			} else {
+				assert.NoError(t, finalState.Err, "Error should be nil")
+			}
+
+			// Check Done channel
+			select {
+			case state, ok := <-item.Done():
+				require.True(t, ok, "Done channel should be readable")
+				assert.Equal(t, tc.expectedOutcome, state.Outcome, "Done channel outcome should match the first call")
+				if tc.expectedErrIs != nil {
+					assert.ErrorIs(t, state.Err, tc.expectedErrIs, "Done channel error should match the first call")
+				} else {
+					assert.NoError(t, state.Err, "Done channel error should be nil")
+				}
+			case <-time.After(50 * time.Millisecond):
+				t.Fatal("Done channel should have received the state")
+			}
+		})
+	}
+}
+
+func TestFlowItem_Finalize_InferOutcome(t *testing.T) {
+	t.Parallel()
+	now := time.Now()
+
+	testCases := []struct {
+		name          string
+		cause         error
+		isQueued      bool
+		expectOutcome types.QueueOutcome
+		expectErrIs   error
+	}{
+		{
+			name:          "queued TTL expired",
+			cause:         types.ErrTTLExpired,
+			isQueued:      true,
+			expectOutcome: types.QueueOutcomeEvictedTTL,
+			expectErrIs:   types.ErrTTLExpired,
+		},
+		{
+			name:          "queued context cancelled",
+			cause:         context.Canceled,
+			isQueued:      true,
+			expectOutcome: types.QueueOutcomeEvictedContextCancelled,
+			expectErrIs:   types.ErrContextCancelled,
+		},
+		{
+			name:          "queued other error",
+			cause:         errors.New("other cause"),
+			isQueued:      true,
+			expectOutcome: types.QueueOutcomeEvictedOther,
+			expectErrIs:   types.ErrEvicted,
+		},
+		{
+			name:          "not queued TTL expired",
+			cause:         types.ErrTTLExpired,
+			isQueued:      false,
+			expectOutcome: types.QueueOutcomeRejectedOther,
+			expectErrIs:   types.ErrTTLExpired,
+		},
+		{
+			name:          "not queued context cancelled",
+			cause:         context.Canceled,
+			isQueued:      false,
+			expectOutcome: types.QueueOutcomeRejectedOther,
+			expectErrIs:   types.ErrContextCancelled,
+		},
+		{
+			name:          "nil cause queued",
+			cause:         nil,
+			isQueued:      true,
+			expectOutcome: types.QueueOutcomeEvictedOther,
+			expectErrIs:   types.ErrEvicted,
+		},
+		{
+			name:          "nil cause not queued",
+			cause:         nil,
+			isQueued:      false,
+			expectOutcome: types.QueueOutcomeRejectedOther,
+			expectErrIs:   types.ErrRejected,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			req := typesmocks.NewMockFlowControlRequest(100, "req-1", types.FlowKey{})
+			item := NewItem(req, time.Minute, now)
+			if tc.isQueued {
+				item.SetHandle(&typesmocks.MockQueueItemHandle{})
+			}
+
+			item.Finalize(tc.cause)
+
+			finalState := item.FinalState()
+			require.NotNil(t, finalState, "FinalState should not be nil")
+			assert.Equal(t, tc.expectOutcome, finalState.Outcome, "Unexpected outcome")
+			require.Error(t, finalState.Err, "An error should be set")
+			assert.ErrorIs(t, finalState.Err, tc.expectErrIs, "Unexpected error type")
+		})
 	}
 }
diff --git a/pkg/epp/flowcontrol/controller/internal/processor_test.go b/pkg/epp/flowcontrol/controller/internal/processor_test.go
index 4c31d7a..2280b82 100644
--- a/pkg/epp/flowcontrol/controller/internal/processor_test.go
+++ b/pkg/epp/flowcontrol/controller/internal/processor_test.go
@@ -100,6 +100,7 @@ func newTestHarness(t *testing.T, expiryCleanupInterval time.Duration) *testHarn
 		queues:             make(map[types.FlowKey]*mocks.MockManagedQueue),
 		priorityFlows:      make(map[int][]types.FlowKey),
 	}
+	h.ctx, h.cancel = context.WithCancel(context.Background())
 
 	// Wire up the harness to provide the mock implementations for the shard's dependencies.
 	h.ManagedQueueFunc = h.managedQueue
@@ -118,7 +119,14 @@ func newTestHarness(t *testing.T, expiryCleanupInterval time.Duration) *testHarn
 		}
 	}
 
-	h.processor = NewShardProcessor(h, h.saturationDetector, h.clock, expiryCleanupInterval, 100, h.logger)
+	h.processor = NewShardProcessor(
+		h.ctx,
+		h,
+		h.saturationDetector,
+		h.clock,
+		expiryCleanupInterval,
+		100,
+		h.logger)
 	require.NotNil(t, h.processor, "NewShardProcessor should not return nil")
 
 	t.Cleanup(func() { h.Stop() })
@@ -170,8 +178,7 @@ func (h *testHarness) waitForFinalization(item *FlowItem) (types.QueueOutcome, e
 // newTestItem creates a new FlowItem for testing purposes.
 func (h *testHarness) newTestItem(id string, key types.FlowKey, ttl time.Duration) *FlowItem {
 	h.t.Helper()
-	ctx := log.IntoContext(context.Background(), h.logger)
-	req := typesmocks.NewMockFlowControlRequest(100, id, key, ctx)
+	req := typesmocks.NewMockFlowControlRequest(100, id, key)
 	return NewItem(req, ttl, h.clock.Now())
 }
 
@@ -365,61 +372,11 @@ func TestShardProcessor(t *testing.T) {
 			h.Start()
 			h.Go()
 			h.Stop() // Stop the processor, then immediately try to enqueue.
-			require.NoError(t, h.processor.Submit(item), "precondition: Submit should not fail, even on shutdown")
+			require.ErrorIs(t, h.processor.Submit(item), types.ErrFlowControllerNotRunning,
+				"Submit should return ErrFlowControllerNotRunning on shutdown")
 
 			// --- ASSERT ---
-			outcome, err := h.waitForFinalization(item)
-			assert.Equal(t, types.QueueOutcomeRejectedOther, outcome, "The outcome should be RejectedOther")
-			require.Error(t, err, "An eviction on shutdown should produce an error")
-			assert.ErrorIs(t, err, types.ErrFlowControllerNotRunning, "The error should be of type ErrFlowControllerNotRunning")
-		})
-
-		t.Run("should evict item on TTL expiry via background cleanup", func(t *testing.T) {
-			t.Parallel()
-			// --- ARRANGE ---
-			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-expired-evict", testFlow, testShortTTL)
-			h.addQueue(testFlow)
-
-			// --- ACT ---
-			h.Start()
-			require.NoError(t, h.processor.Submit(item), "precondition: Submit should not fail")
-			h.Go()
-
-			h.clock.Step(testShortTTL * 2) // Let time pass for the item to expire.
-			// Manually invoke the cleanup logic to simulate a tick of the cleanup loop deterministically.
-			h.processor.cleanupExpired(h.clock.Now())
-
-			// --- ASSERT ---
-			outcome, err := h.waitForFinalization(item)
-			assert.Equal(t, types.QueueOutcomeEvictedTTL, outcome, "The final outcome should be EvictedTTL")
-			require.Error(t, err, "A TTL eviction should produce an error")
-			assert.ErrorIs(t, err, types.ErrTTLExpired, "The error should be of type ErrTTLExpired")
-		})
-
-		t.Run("should evict item on context cancellation", func(t *testing.T) {
-			t.Parallel()
-			// --- ARRANGE ---
-			h := newTestHarness(t, testCleanupTick)
-			ctx, cancel := context.WithCancel(context.Background())
-			req := typesmocks.NewMockFlowControlRequest(100, "req-ctx-cancel", testFlow, ctx)
-			item := NewItem(req, testTTL, h.clock.Now())
-			h.addQueue(testFlow)
-
-			// --- ACT ---
-			h.Start()
-			require.NoError(t, h.processor.Submit(item), "precondition: Submit should not fail")
-			h.Go()
-			cancel() // Cancel the context after the item is enqueued.
-			// Manually invoke the cleanup logic to deterministically check for the cancelled context.
-			h.processor.cleanupExpired(h.clock.Now())
-
-			// --- ASSERT ---
-			outcome, err := h.waitForFinalization(item)
-			assert.Equal(t, types.QueueOutcomeEvictedContextCancelled, outcome,
-				"The outcome should be EvictedContextCancelled")
-			require.Error(t, err, "A context cancellation eviction should produce an error")
-			assert.ErrorIs(t, err, types.ErrContextCancelled, "The error should be of type ErrContextCancelled")
+			assert.Nil(t, item.FinalState(), "Item should not be finalized by the processor")
 		})
 
 		t.Run("should evict a queued item on shutdown", func(t *testing.T) {
@@ -444,7 +401,8 @@ func TestShardProcessor(t *testing.T) {
 			outcome, err := h.waitForFinalization(item)
 			assert.Equal(t, types.QueueOutcomeEvictedOther, outcome, "The outcome should be EvictedOther")
 			require.Error(t, err, "An eviction on shutdown should produce an error")
-			assert.ErrorIs(t, err, types.ErrFlowControllerNotRunning, "The error should be of type ErrFlowControllerNotRunning")
+			assert.ErrorIs(t, err, types.ErrFlowControllerNotRunning,
+				"The error should be of type ErrFlowControllerNotRunning")
 		})
 
 		t.Run("should handle concurrent enqueues and dispatch all items", func(t *testing.T) {
@@ -454,7 +412,7 @@ func TestShardProcessor(t *testing.T) {
 			const numConcurrentItems = 20
 			q := h.addQueue(testFlow)
 			itemsToTest := make([]*FlowItem, 0, numConcurrentItems)
-			for i := 0; i < numConcurrentItems; i++ {
+			for i := range numConcurrentItems {
 				item := h.newTestItem(fmt.Sprintf("req-concurrent-%d", i), testFlow, testTTL)
 				itemsToTest = append(itemsToTest, item)
 			}
@@ -495,16 +453,26 @@ func TestShardProcessor(t *testing.T) {
 			// Use channels to pause the dispatch cycle right before it would remove the item.
 			policyCanProceed := make(chan struct{})
 			itemIsBeingDispatched := make(chan struct{})
+			var signalOnce sync.Once
+			var removedItem types.QueueItemAccessor
 
 			require.NoError(t, q.Add(item)) // Add the item directly to the queue.
 
 			// Override the queue's `RemoveFunc` to pause the dispatch goroutine at a critical moment.
 			q.RemoveFunc = func(h types.QueueItemHandle) (types.QueueItemAccessor, error) {
-				close(itemIsBeingDispatched) // 1. Signal that dispatch is happening.
-				<-policyCanProceed           // 2. Wait for the test to tell us to continue.
-				// 4. After we unblock, the item will have already been finalized by the cleanup logic, so we simulate the
-				//    real-world outcome of a failed remove.
-				return nil, fmt.Errorf("item with handle %v not found", h)
+				var err error
+				signalOnce.Do(func() {
+					removedItem = item
+					close(itemIsBeingDispatched) // 1. Signal that dispatch is happening.
+					<-policyCanProceed           // 2. Wait for the test to tell us to continue.
+					// 4. After we unblock, the item will have already been finalized by the cleanup logic.
+					// We simulate the item no longer being found.
+					err = fmt.Errorf("item with handle %v not found", h)
+				})
+				if removedItem == item {
+					return item, nil // Return the item on the first call
+				}
+				return nil, err // Return error on subsequent calls
 			}
 
 			// --- ACT ---
@@ -513,20 +481,23 @@ func TestShardProcessor(t *testing.T) {
 			h.Go()
 
 			// Wait for the dispatch cycle to select our item and pause inside our mock `RemoveFunc`.
-			<-itemIsBeingDispatched
+			select {
+			case <-itemIsBeingDispatched:
+			case <-time.After(testWaitTimeout):
+				t.Fatal("Timed out waiting for item to be dispatched")
+			}
 
 			// 3. The dispatch goroutine is now paused. We can now safely win the "race" by running cleanup logic.
 			h.clock.Step(testShortTTL * 2)
-			h.processor.cleanupExpired(h.clock.Now()) // This will remove and finalize the item.
+			item.Finalize(types.ErrTTLExpired) // This will finalize the item with RejectedOther.
 
-			// 5. Un-pause the dispatch goroutine. It will now fail to remove the item and the `dispatchCycle` will
-			//    correctly conclude without finalizing the item a second time.
+			// 5. Un-pause the dispatch goroutine.
 			close(policyCanProceed)
 
 			// --- ASSERT ---
-			// The item's final state should be from the cleanup logic (EvictedTTL), not the dispatch logic.
+			// The item's final state should be from the Finalize call above.
 			outcome, err := h.waitForFinalization(item)
-			assert.Equal(t, types.QueueOutcomeEvictedTTL, outcome, "The outcome should be EvictedTTL from the cleanup routine")
+			assert.Equal(t, types.QueueOutcomeEvictedTTL, outcome, "The outcome should be EvictedTTL from the Finalize call")
 			require.Error(t, err, "A TTL eviction should produce an error")
 			assert.ErrorIs(t, err, types.ErrTTLExpired, "The error should be of type ErrTTLExpired")
 		})
@@ -594,9 +565,10 @@ func TestShardProcessor(t *testing.T) {
 						h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) { return nil, testErr }
 					},
 					assert: func(t *testing.T, h *testHarness, item *FlowItem) {
-						assert.Equal(t, types.QueueOutcomeRejectedOther, item.finalState.Outcome, "Outcome should be RejectedOther")
-						require.Error(t, item.finalState.Err, "An error should be returned")
-						assert.ErrorIs(t, item.finalState.Err, testErr, "The underlying error should be preserved")
+						assert.Equal(t, types.QueueOutcomeRejectedOther, item.FinalState().Outcome,
+							"Outcome should be RejectedOther")
+						require.Error(t, item.FinalState().Err, "An error should be returned")
+						assert.ErrorIs(t, item.FinalState().Err, testErr, "The underlying error should be preserved")
 					},
 				},
 				{
@@ -606,9 +578,10 @@ func TestShardProcessor(t *testing.T) {
 						h.PriorityBandAccessorFunc = func(int) (framework.PriorityBandAccessor, error) { return nil, testErr }
 					},
 					assert: func(t *testing.T, h *testHarness, item *FlowItem) {
-						assert.Equal(t, types.QueueOutcomeRejectedOther, item.finalState.Outcome, "Outcome should be RejectedOther")
-						require.Error(t, item.finalState.Err, "An error should be returned")
-						assert.ErrorIs(t, item.finalState.Err, testErr, "The underlying error should be preserved")
+						assert.Equal(t, types.QueueOutcomeRejectedOther, item.FinalState().Outcome,
+							"Outcome should be RejectedOther")
+						require.Error(t, item.FinalState().Err, "An error should be returned")
+						assert.ErrorIs(t, item.FinalState().Err, testErr, "The underlying error should be preserved")
 					},
 				},
 				{
@@ -618,9 +591,10 @@ func TestShardProcessor(t *testing.T) {
 						mockQueue.AddFunc = func(types.QueueItemAccessor) error { return testErr }
 					},
 					assert: func(t *testing.T, h *testHarness, item *FlowItem) {
-						assert.Equal(t, types.QueueOutcomeRejectedOther, item.finalState.Outcome, "Outcome should be RejectedOther")
-						require.Error(t, item.finalState.Err, "An error should be returned")
-						assert.ErrorIs(t, item.finalState.Err, testErr, "The underlying error should be preserved")
+						assert.Equal(t, types.QueueOutcomeRejectedOther, item.FinalState().Outcome,
+							"Outcome should be RejectedOther")
+						require.Error(t, item.FinalState().Err, "An error should be returned")
+						assert.ErrorIs(t, item.FinalState().Err, testErr, "The underlying error should be preserved")
 					},
 				},
 				{
@@ -640,13 +614,13 @@ func TestShardProcessor(t *testing.T) {
 					item: func() *FlowItem {
 						// Create a pre-finalized item.
 						item := newTestHarness(t, 0).newTestItem("req-finalized", testFlow, testTTL)
-						item.Finalize(types.QueueOutcomeDispatched, nil)
+						item.FinalizeWithOutcome(types.QueueOutcomeDispatched, nil)
 						return item
 					}(),
 					assert: func(t *testing.T, h *testHarness, item *FlowItem) {
 						// The item was already finalized, so its state should not change.
-						assert.Equal(t, types.QueueOutcomeDispatched, item.finalState.Outcome, "Outcome should remain unchanged")
-						assert.NoError(t, item.finalState.Err, "Error should remain unchanged")
+						assert.Equal(t, types.QueueOutcomeDispatched, item.FinalState().Outcome, "Outcome should remain unchanged")
+						assert.NoError(t, item.FinalState().Err, "Error should remain unchanged")
 					},
 				},
 			}
@@ -905,9 +879,9 @@ func TestShardProcessor(t *testing.T) {
 
 				// Verify all high-priority items are gone and low-priority items remain.
 				for _, item := range highPrioItems {
-					assert.Equal(t, types.QueueOutcomeDispatched, item.finalState.Outcome,
+					assert.Equal(t, types.QueueOutcomeDispatched, item.FinalState().Outcome,
 						"High-priority item should be dispatched")
-					assert.NoError(t, item.finalState.Err, "Dispatched high-priority item should not have an error")
+					assert.NoError(t, item.FinalState().Err, "Dispatched high-priority item should not have an error")
 				}
 				assert.Equal(t, numItems, qLow.Len(), "Low-priority queue should still be full")
 
@@ -967,11 +941,12 @@ func TestShardProcessor(t *testing.T) {
 				}
 			})
 
-			t.Run("should evict item that expires at moment of dispatch", func(t *testing.T) {
+			t.Run("should not dispatch already finalized item", func(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				item := h.newTestItem("req-expired-dispatch", testFlow, testShortTTL)
+				item := h.newTestItem("req-already-finalized", testFlow, testTTL)
+				item.FinalizeWithOutcome(types.QueueOutcomeRejectedOther, errors.New("already done"))
 
 				h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) {
 					return &mocks.MockManagedQueue{
@@ -982,43 +957,61 @@ func TestShardProcessor(t *testing.T) {
 				}
 
 				// --- ACT ---
-				h.clock.Step(testShortTTL * 2) // Make the item expire.
 				err := h.processor.dispatchItem(item, h.logger)
 
 				// --- ASSERT ---
-				// First, check the error returned by `dispatchItem`.
-				require.Error(t, err, "dispatchItem should return an error for an expired item")
-				assert.ErrorIs(t, err, types.ErrTTLExpired, "The error should be of type ErrTTLExpired")
-
-				// Second, check the final state of the item itself.
-				assert.Equal(t, types.QueueOutcomeEvictedTTL, item.finalState.Outcome,
-					"The item's final outcome should be EvictedTTL")
-				require.Error(t, item.finalState.Err, "The item's final state should contain an error")
-				assert.ErrorIs(t, item.finalState.Err, types.ErrTTLExpired,
-					"The item's final error should be of type ErrTTLExpired")
+				require.NoError(t, err, "dispatchItem should return no error for an already finalized item")
+
+				// Check the final state of the item itself - it should not have changed.
+				finalState := item.FinalState()
+				require.NotNil(t, finalState, "Item must be finalized")
+				assert.Equal(t, types.QueueOutcomeRejectedOther, finalState.Outcome,
+					"The item's final outcome should be RejectedOther")
+				assert.ErrorContains(t, finalState.Err, "already done",
+					"The error should be the one from the first Finalize call")
 			})
 		})
 
 		t.Run("cleanup and utility methods", func(t *testing.T) {
 			t.Parallel()
 
-			t.Run("should remove and finalize expired items", func(t *testing.T) {
+			t.Run("should sweep externally finalized items", func(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				// Create an item that is already expired relative to the cleanup time.
-				item := h.newTestItem("req-expired", testFlow, 1*time.Millisecond)
+				item := h.newTestItem("req-external-finalized", testFlow, testTTL)
 				q := h.addQueue(testFlow)
-				require.NoError(t, q.Add(item))
-				cleanupTime := h.clock.Now().Add(10 * time.Millisecond)
+				require.NoError(t, q.Add(item), "Failed to add item to queue")
+
+				// Externally finalize the item
+				item.Finalize(context.Canceled)
+				require.NotNil(t, item.FinalState(), "Item should be finalized")
 
 				// --- ACT ---
-				h.processor.cleanupExpired(cleanupTime)
+				h.processor.sweepFinalizedItems()
 
 				// --- ASSERT ---
-				assert.Equal(t, types.QueueOutcomeEvictedTTL, item.finalState.Outcome, "Item outcome should be EvictedTTL")
-				require.Error(t, item.finalState.Err, "Item should have an error")
-				assert.ErrorIs(t, item.finalState.Err, types.ErrTTLExpired, "Item error should be ErrTTLExpired")
+				assert.Equal(t, 0, q.Len(), "Queue should be empty after sweep")
+				finalState := item.FinalState()
+				assert.Equal(t, types.QueueOutcomeEvictedContextCancelled, finalState.Outcome,
+					"Outcome should be EvictedContextCancelled")
+				assert.ErrorIs(t, finalState.Err, types.ErrContextCancelled, "Error should be ErrContextCancelled")
+			})
+
+			t.Run("should not sweep items not finalized", func(t *testing.T) {
+				t.Parallel()
+				// --- ARRANGE ---
+				h := newTestHarness(t, testCleanupTick)
+				item := h.newTestItem("req-not-finalized", testFlow, testTTL)
+				q := h.addQueue(testFlow)
+				require.NoError(t, q.Add(item), "Failed to add item to queue")
+
+				// --- ACT ---
+				h.processor.sweepFinalizedItems()
+
+				// --- ASSERT ---
+				assert.Equal(t, 1, q.Len(), "Queue should still contain the item")
+				assert.Nil(t, item.FinalState(), "Item should not be finalized")
 			})
 
 			t.Run("should evict all items on shutdown", func(t *testing.T) {
@@ -1033,9 +1026,10 @@ func TestShardProcessor(t *testing.T) {
 				h.processor.evictAll()
 
 				// --- ASSERT ---
-				assert.Equal(t, types.QueueOutcomeEvictedOther, item.finalState.Outcome, "Item outcome should be EvictedOther")
-				require.Error(t, item.finalState.Err, "Item should have an error")
-				assert.ErrorIs(t, item.finalState.Err, types.ErrFlowControllerNotRunning,
+				assert.Equal(t, types.QueueOutcomeEvictedOther, item.FinalState().Outcome,
+					"Item outcome should be EvictedOther")
+				require.Error(t, item.FinalState().Err, "Item should have an error")
+				assert.ErrorIs(t, item.FinalState().Err, types.ErrFlowControllerNotRunning,
 					"Item error should be ErrFlowControllerNotRunning")
 			})
 
@@ -1055,25 +1049,6 @@ func TestShardProcessor(t *testing.T) {
 				}, "processAllQueuesConcurrently should not panic on registry errors")
 			})
 
-			t.Run("should handle items of an unexpected type gracefully during finalization", func(t *testing.T) {
-				t.Parallel()
-				// --- ARRANGE ---
-				h := newTestHarness(t, testCleanupTick)
-				item := &typesmocks.MockQueueItemAccessor{
-					OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "bad-item", testFlow, context.Background()),
-				}
-				items := []types.QueueItemAccessor{item}
-
-				// --- ACT & ASSERT ---
-				// The test passes if this call completes without panicking.
-				assert.NotPanics(t, func() {
-					getOutcome := func(types.QueueItemAccessor) (types.QueueOutcome, error) {
-						return types.QueueOutcomeEvictedOther, nil
-					}
-					h.processor.finalizeItems(items, h.logger, getOutcome)
-				}, "finalizeItems should not panic on unexpected item types")
-			})
-
 			t.Run("should process all queues with a worker pool", func(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
@@ -1122,6 +1097,26 @@ func TestShardProcessor(t *testing.T) {
 				require.Error(t, err, "Submit must return an error when the channel is full")
 				assert.ErrorIs(t, err, ErrProcessorBusy, "The returned error must be ErrProcessorBusy")
 			})
+
+			t.Run("should return ErrFlowControllerNotRunning if lifecycleCtx is cancelled", func(t *testing.T) {
+				t.Parallel()
+				h := newTestHarness(t, testCleanupTick)
+				h.Start()
+				h.Go()     // Ensure the Run loop has started
+				h.cancel() // Cancel the lifecycle context
+				h.Stop()   // Wait for the processor to fully stop
+
+				item := h.newTestItem("item-ctx-cancel", testFlow, testTTL)
+				err := h.processor.Submit(item)
+				require.ErrorIs(t, err, types.ErrFlowControllerNotRunning,
+					"Submit must return ErrFlowControllerNotRunning when lifecycleCtx is cancelled")
+				assert.Nil(t, item.FinalState(), "Item should not be finalized by Submit")
+
+				err = h.processor.SubmitOrBlock(context.Background(), item)
+				require.ErrorIs(t, err, types.ErrFlowControllerNotRunning,
+					"SubmitOrBlock must return ErrFlowControllerNotRunning when lifecycleCtx is cancelled")
+				assert.Nil(t, item.FinalState(), "Item should not be finalized by SubmitOrBlock")
+			})
 		})
 
 		t.Run("SubmitOrBlock", func(t *testing.T) {
@@ -1195,114 +1190,9 @@ func TestShardProcessor(t *testing.T) {
 				require.Error(t, err, "SubmitOrBlock should return an error when shutting down")
 				assert.ErrorIs(t, err, types.ErrFlowControllerNotRunning, "The error should be ErrFlowControllerNotRunning")
 
-				outcome, err := h.waitForFinalization(item)
-				assert.Equal(t, types.QueueOutcomeRejectedOther, outcome, "The outcome should be RejectedOther")
-				require.Error(t, err, "Finalization should include an error")
-				assert.ErrorIs(t, err, types.ErrFlowControllerNotRunning,
-					"The finalization error should be ErrFlowControllerNotRunning")
+				// Item should not be finalized by the processor
+				assert.Nil(t, item.FinalState(), "Item should not be finalized by the processor")
 			})
 		})
 	})
 }
-
-func TestCheckItemExpiry(t *testing.T) {
-	t.Parallel()
-
-	// --- ARRANGE ---
-	now := time.Now()
-	ctxCancelled, cancel := context.WithCancel(context.Background())
-	cancel() // Cancel the context immediately.
-
-	testCases := []struct {
-		name          string
-		item          types.QueueItemAccessor
-		now           time.Time
-		expectExpired bool
-		expectOutcome types.QueueOutcome
-		expectErr     error
-	}{
-		{
-			name: "should not be expired if TTL is not reached and context is active",
-			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-not-expired", testFlow, context.Background()),
-				testTTL,
-				now),
-			now:           now.Add(30 * time.Second),
-			expectExpired: false,
-			expectOutcome: types.QueueOutcomeNotYetFinalized,
-			expectErr:     nil,
-		},
-		{
-			name: "should not be expired if TTL is disabled (0)",
-			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-not-expired-no-ttl", testFlow, context.Background()),
-				0,
-				now),
-			now:           now.Add(30 * time.Second),
-			expectExpired: false,
-			expectOutcome: types.QueueOutcomeNotYetFinalized,
-			expectErr:     nil,
-		},
-		{
-			name: "should be expired if TTL is exceeded",
-			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-ttl-expired", testFlow, context.Background()),
-				time.Second,
-				now),
-			now:           now.Add(2 * time.Second),
-			expectExpired: true,
-			expectOutcome: types.QueueOutcomeEvictedTTL,
-			expectErr:     types.ErrTTLExpired,
-		},
-		{
-			name: "should be expired if context is cancelled",
-			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-ctx-cancelled", testFlow, ctxCancelled),
-				testTTL,
-				now),
-			now:           now,
-			expectExpired: true,
-			expectOutcome: types.QueueOutcomeEvictedContextCancelled,
-			expectErr:     types.ErrContextCancelled,
-		},
-		{
-			name: "should be expired if already finalized",
-			item: func() types.QueueItemAccessor {
-				i := NewItem(
-					typesmocks.NewMockFlowControlRequest(100, "req-finalized", testFlow, context.Background()),
-					testTTL,
-					now)
-				i.Finalize(types.QueueOutcomeDispatched, nil)
-				return i
-			}(),
-			now:           now,
-			expectExpired: true,
-			expectOutcome: types.QueueOutcomeDispatched,
-			expectErr:     nil,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-			// --- ACT ---
-			isExpired, outcome, err := checkItemExpiry(tc.item, tc.now)
-
-			// --- ASSERT ---
-			assert.Equal(t, tc.expectExpired, isExpired, "Expired status should match expected value")
-			assert.Equal(t, tc.expectOutcome, outcome, "Outcome should match expected value")
-
-			if tc.expectErr != nil {
-				require.Error(t, err, "An error was expected")
-				// Use ErrorIs for sentinel errors, ErrorContains for general messages.
-				if errors.Is(tc.expectErr, types.ErrTTLExpired) || errors.Is(tc.expectErr, types.ErrContextCancelled) {
-					assert.ErrorIs(t, err, tc.expectErr, "The specific error type should be correct")
-				} else {
-					assert.ErrorContains(t, err, tc.expectErr.Error(), "The error message should contain the expected text")
-				}
-			} else {
-				assert.NoError(t, err, "No error was expected")
-			}
-		})
-	}
-}
diff --git a/pkg/epp/handlers/request_test.go b/pkg/epp/handlers/request_test.go
index 4ae2078..a3ef90c 100644
--- a/pkg/epp/handlers/request_test.go
+++ b/pkg/epp/handlers/request_test.go
@@ -21,6 +21,7 @@ import (
 
 	configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
+	"github.com/stretchr/testify/assert"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 )
 
@@ -66,3 +67,32 @@ func TestHandleRequestHeaders(t *testing.T) {
 		t.Errorf("expected fairness ID header to be removed from request headers, but it was not")
 	}
 }
+
+func TestHandleRequestHeaders_DefaultFairnessID(t *testing.T) {
+	t.Parallel()
+
+	server := &StreamingServer{}
+	reqCtx := &RequestContext{
+		Request: &Request{
+			Headers: make(map[string]string),
+		},
+	}
+
+	req := &extProcPb.ProcessingRequest_RequestHeaders{
+		RequestHeaders: &extProcPb.HttpHeaders{
+			Headers: &configPb.HeaderMap{
+				Headers: []*configPb.HeaderValue{
+					{
+						Key:   "x-test-header",
+						Value: "test-value",
+					},
+				},
+			},
+			EndOfStream: false,
+		},
+	}
+
+	err := server.HandleRequestHeaders(reqCtx, req)
+	assert.NoError(t, err, "expected no error")
+	assert.Equal(t, defaultFairnessID, reqCtx.FairnessID, "expected fairness ID to be defaulted")
+}
diff --git a/pkg/epp/metrics/metrics_test.go b/pkg/epp/metrics/metrics_test.go
index 837fb8b..28f9416 100644
--- a/pkg/epp/metrics/metrics_test.go
+++ b/pkg/epp/metrics/metrics_test.go
@@ -22,6 +22,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prometheus/client_golang/prometheus"
+	dto "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/require"
 	"k8s.io/component-base/metrics/testutil"
 	"sigs.k8s.io/controller-runtime/pkg/metrics"
 
@@ -44,7 +47,14 @@ const (
 	PerPodQueueSizeMetrics             = InferencePoolComponent + "_per_pod_queue_size"
 )
 
+func TestMain(m *testing.M) {
+	// Register all metrics once for the entire test suite.
+	Register()
+	os.Exit(m.Run())
+}
+
 func TestRecordRequestCounterandSizes(t *testing.T) {
+	Reset()
 	type requests struct {
 		modelName       string
 		targetModelName string
@@ -78,7 +88,6 @@ func TestRecordRequestCounterandSizes(t *testing.T) {
 			},
 		},
 	}}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, req := range scenario.reqs {
@@ -114,6 +123,7 @@ func TestRecordRequestCounterandSizes(t *testing.T) {
 }
 
 func TestRecordRequestErrorCounter(t *testing.T) {
+	Reset()
 	type requests struct {
 		modelName       string
 		targetModelName string
@@ -150,7 +160,6 @@ func TestRecordRequestErrorCounter(t *testing.T) {
 			},
 		},
 	}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, req := range scenario.reqs {
@@ -174,6 +183,7 @@ func TestRecordRequestErrorCounter(t *testing.T) {
 }
 
 func TestRecordRequestLatencies(t *testing.T) {
+	Reset()
 	ctx := logutil.NewTestLoggerIntoContext(context.Background())
 	timeBaseline := time.Now()
 	type requests struct {
@@ -229,7 +239,6 @@ func TestRecordRequestLatencies(t *testing.T) {
 			invalid: true,
 		},
 	}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, req := range scenario.reqs {
@@ -256,6 +265,7 @@ func TestRecordRequestLatencies(t *testing.T) {
 }
 
 func TestRecordNormalizedTimePerOutputToken(t *testing.T) {
+	Reset()
 	ctx := logutil.NewTestLoggerIntoContext(context.Background())
 	timeBaseline := time.Now()
 	type tokenRequests struct {
@@ -330,7 +340,6 @@ func TestRecordNormalizedTimePerOutputToken(t *testing.T) {
 			invalid: true,
 		},
 	}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, req := range scenario.reqs {
@@ -357,6 +366,7 @@ func TestRecordNormalizedTimePerOutputToken(t *testing.T) {
 }
 
 func TestRecordResponseMetrics(t *testing.T) {
+	Reset()
 	type responses struct {
 		modelName       string
 		targetModelName string
@@ -400,7 +410,6 @@ func TestRecordResponseMetrics(t *testing.T) {
 			},
 		},
 	}}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, resp := range scenario.resp {
@@ -451,6 +460,7 @@ func TestRecordResponseMetrics(t *testing.T) {
 }
 
 func TestRunningRequestsMetrics(t *testing.T) {
+	Reset()
 	type request struct {
 		modelName string
 		complete  bool // true -> request is completed, false -> running request
@@ -483,7 +493,6 @@ func TestRunningRequestsMetrics(t *testing.T) {
 		},
 	}
 
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, req := range scenario.requests {
@@ -511,6 +520,7 @@ func TestRunningRequestsMetrics(t *testing.T) {
 }
 
 func TestInferencePoolMetrics(t *testing.T) {
+	Reset()
 	scenarios := []struct {
 		name         string
 		poolName     string
@@ -524,7 +534,6 @@ func TestInferencePoolMetrics(t *testing.T) {
 			queueSizeAvg: 0.4,
 		},
 	}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			RecordInferencePoolAvgKVCache(scenario.poolName, scenario.kvCacheAvg)
@@ -560,6 +569,7 @@ func TestInferencePoolMetrics(t *testing.T) {
 }
 
 func TestPluginProcessingLatencies(t *testing.T) {
+	Reset()
 	type pluginLatency struct {
 		extensionPoint string
 		pluginType     string
@@ -600,7 +610,6 @@ func TestPluginProcessingLatencies(t *testing.T) {
 			},
 		},
 	}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, latency := range scenario.latencies {
@@ -624,6 +633,7 @@ func TestPluginProcessingLatencies(t *testing.T) {
 }
 
 func TestSchedulerE2ELatency(t *testing.T) {
+	Reset()
 	scenarios := []struct {
 		name      string
 		durations []time.Duration
@@ -643,7 +653,6 @@ func TestSchedulerE2ELatency(t *testing.T) {
 			},
 		},
 	}
-	Register()
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			for _, duration := range scenario.durations {
@@ -667,6 +676,7 @@ func TestSchedulerE2ELatency(t *testing.T) {
 }
 
 func TestPrefixCacheMetrics(t *testing.T) {
+	Reset()
 	const (
 		PrefixCacheSizeMetric      = InferenceExtension + "_prefix_indexer_size"
 		PrefixCacheHitRatioMetric  = InferenceExtension + "_prefix_indexer_hit_ratio"
@@ -713,7 +723,6 @@ func TestPrefixCacheMetrics(t *testing.T) {
 		},
 	}
 
-	Register()
 	t.Run(scenario.name, func(t *testing.T) {
 		// Record cache size metrics
 		for _, size := range scenario.cacheSizes {
@@ -768,3 +777,104 @@ func TestPrefixCacheMetrics(t *testing.T) {
 		}
 	})
 }
+
+func getHistogramVecLabelValues(t *testing.T, h *prometheus.HistogramVec, labelValues ...string) (*dto.Histogram, error) {
+	t.Helper()
+	m, err := h.GetMetricWithLabelValues(labelValues...)
+	if err != nil {
+		return nil, err
+	}
+	metricDto := &dto.Metric{}
+	if err := m.(prometheus.Histogram).Write(metricDto); err != nil {
+		return nil, err
+	}
+	return metricDto.GetHistogram(), nil
+}
+
+func TestFlowControlQueueDurationMetric(t *testing.T) {
+	Reset()
+
+	records := []struct {
+		fairnessID string
+		priority   string
+		outcome    string
+		duration   time.Duration
+	}{
+		{fairnessID: "user-a", priority: "100", outcome: "Dispatched", duration: 10 * time.Millisecond},
+		{fairnessID: "user-a", priority: "100", outcome: "Dispatched", duration: 20 * time.Millisecond},
+		{fairnessID: "user-b", priority: "100", outcome: "RejectedCapacity", duration: 5 * time.Millisecond},
+		{fairnessID: "user-a", priority: "50", outcome: "Dispatched", duration: 100 * time.Millisecond},
+	}
+
+	for _, rec := range records {
+		RecordFlowControlRequestQueueDuration(rec.fairnessID, rec.priority, rec.outcome, rec.duration)
+	}
+
+	testCases := []struct {
+		name        string
+		labels      prometheus.Labels
+		expectCount uint64
+		expectSum   float64
+	}{
+		{
+			name:        "user-a, prio 100, dispatched",
+			labels:      prometheus.Labels{"fairness_id": "user-a", "priority": "100", "outcome": "Dispatched"},
+			expectCount: 2,
+			expectSum:   0.03, // 0.01 + 0.02
+		},
+		{
+			name:        "user-b, prio 100, rejected",
+			labels:      prometheus.Labels{"fairness_id": "user-b", "priority": "100", "outcome": "RejectedCapacity"},
+			expectCount: 1,
+			expectSum:   0.005,
+		},
+		{
+			name:        "user-a, prio 50, dispatched",
+			labels:      prometheus.Labels{"fairness_id": "user-a", "priority": "50", "outcome": "Dispatched"},
+			expectCount: 1,
+			expectSum:   0.1,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			labels := []string{tc.labels["fairness_id"], tc.labels["priority"], tc.labels["outcome"]}
+			hist, err := getHistogramVecLabelValues(t, flowControlRequestQueueDuration, labels...)
+			require.NoError(t, err, "Failed to get histogram for labels %v", tc.labels)
+			require.Equal(t, tc.expectCount, hist.GetSampleCount(), "Sample count mismatch for labels %v", tc.labels)
+			require.InDelta(t, tc.expectSum, hist.GetSampleSum(), 0.00001, "Sample sum mismatch for labels %v", tc.labels)
+		})
+	}
+}
+
+func TestFlowControlQueueSizeMetric(t *testing.T) {
+	Reset()
+
+	// Basic Inc/Dec
+	IncFlowControlQueueSize("user-a", "100")
+	val, err := testutil.GetGaugeMetricValue(flowControlQueueSize.WithLabelValues("user-a", "100"))
+	require.NoError(t, err, "Failed to get gauge value for user-a/100 after Inc")
+	require.Equal(t, 1.0, val, "Gauge value should be 1 after Inc for user-a/100")
+
+	DecFlowControlQueueSize("user-a", "100")
+	val, err = testutil.GetGaugeMetricValue(flowControlQueueSize.WithLabelValues("user-a", "100"))
+	require.NoError(t, err, "Failed to get gauge value for user-a/100 after Dec")
+	require.Equal(t, 0.0, val, "Gauge value should be 0 after Dec for user-a/100")
+
+	// Multiple labels
+	IncFlowControlQueueSize("user-b", "200")
+	IncFlowControlQueueSize("user-b", "200")
+	val, err = testutil.GetGaugeMetricValue(flowControlQueueSize.WithLabelValues("user-b", "200"))
+	require.NoError(t, err, "Failed to get gauge value for user-b/200")
+	require.Equal(t, 2.0, val, "Gauge value should be 2 for user-b/200")
+
+	DecFlowControlQueueSize("user-b", "200")
+	val, err = testutil.GetGaugeMetricValue(flowControlQueueSize.WithLabelValues("user-b", "200"))
+	require.NoError(t, err, "Failed to get gauge value for user-b/200 after one Dec")
+	require.Equal(t, 1.0, val, "Gauge value should be 1 for user-b/200 after one Dec")
+
+	// Non-existent labels
+	val, err = testutil.GetGaugeMetricValue(flowControlQueueSize.WithLabelValues("user-c", "100"))
+	require.NoError(t, err, "Failed to get gauge value for non-existent user-c/100")
+	require.Equal(t, 0.0, val, "Gauge value for non-existent labels should be 0")
+}
diff --git a/pkg/epp/requestcontrol/admission_test.go b/pkg/epp/requestcontrol/admission_test.go
new file mode 100644
index 0000000..0857782
--- /dev/null
+++ b/pkg/epp/requestcontrol/admission_test.go
@@ -0,0 +1,282 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package requestcontrol
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	fctypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
+	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"
+)
+
+// --- Mocks ---
+
+type mockSaturationDetector struct {
+	isSaturated bool
+}
+
+func (m *mockSaturationDetector) IsSaturated(_ context.Context, _ []backendmetrics.PodMetrics) bool {
+	return m.isSaturated
+}
+
+type mockFlowController struct {
+	outcome fctypes.QueueOutcome
+	err     error
+	called  bool
+}
+
+func (m *mockFlowController) EnqueueAndWait(
+	_ context.Context,
+	_ fctypes.FlowControlRequest,
+) (fctypes.QueueOutcome, error) {
+	m.called = true
+	return m.outcome, m.err
+}
+
+func TestLegacyAdmissionController_Admit(t *testing.T) {
+	t.Parallel()
+	ctx := logutil.NewTestLoggerIntoContext(context.Background())
+	candidatePods := []backendmetrics.PodMetrics{}
+	reqCtx := &handlers.RequestContext{
+		SchedulingRequest: &schedulingtypes.LLMRequest{RequestId: "test-req"},
+	}
+
+	testCases := []struct {
+		name            string
+		priority        int
+		isSaturated     bool
+		expectErr       bool
+		expectErrCode   string
+		expectErrSubstr string
+	}{
+		{
+			name:        "non_sheddable_saturated_admit",
+			priority:    0,
+			isSaturated: true,
+			expectErr:   false,
+		},
+		{
+			name:        "sheddable_not_saturated_admit",
+			priority:    -1,
+			isSaturated: false,
+			expectErr:   false,
+		},
+		{
+			name:            "sheddable_saturated_reject",
+			priority:        -1,
+			isSaturated:     true,
+			expectErr:       true,
+			expectErrCode:   errutil.InferencePoolResourceExhausted,
+			expectErrSubstr: "system saturated, sheddable request dropped",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			saturationDetector := &mockSaturationDetector{isSaturated: tc.isSaturated}
+			ac := NewLegacyAdmissionController(saturationDetector)
+
+			err := ac.Admit(ctx, reqCtx, candidatePods, tc.priority)
+
+			if !tc.expectErr {
+				assert.NoError(t, err, "Admit() should not have returned an error for scenario: %s", tc.name)
+			} else {
+				require.Error(t, err, "Admit() should have returned an error for scenario: %s", tc.name)
+				var e errutil.Error
+				if assert.ErrorAs(t, err, &e, "error should be of type errutil.Error") {
+					assert.Equal(t, tc.expectErrCode, e.Code, "incorrect error code for scenario: %s", tc.name)
+					assert.Contains(t, e.Msg, tc.expectErrSubstr, "incorrect error message substring for scenario: %s", tc.name)
+				}
+			}
+		})
+	}
+}
+
+func TestFlowControlRequestAdapter(t *testing.T) {
+	t.Parallel()
+	candidatePods := []backendmetrics.PodMetrics{&backendmetrics.FakePodMetrics{}}
+
+	testCases := []struct {
+		name            string
+		requestID       string
+		fairnessID      string
+		priority        int
+		requestByteSize uint64
+		expectFlowKey   fctypes.FlowKey
+	}{
+		{
+			name:            "simple",
+			requestID:       "req-1",
+			fairnessID:      "flow-1",
+			priority:        10,
+			requestByteSize: 1024,
+			expectFlowKey:   fctypes.FlowKey{ID: "flow-1", Priority: 10},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			fcReq := &flowControlRequest{
+				requestID:       tc.requestID,
+				fairnessID:      tc.fairnessID,
+				priority:        tc.priority,
+				requestByteSize: tc.requestByteSize,
+				candidatePods:   candidatePods,
+			}
+
+			assert.Equal(t, tc.requestID, fcReq.ID(), "ID() mismatch")
+			assert.Equal(t, tc.requestByteSize, fcReq.ByteSize(), "ByteSize() mismatch")
+			assert.Equal(t, candidatePods, fcReq.CandidatePodsForScheduling(), "CandidatePodsForScheduling() mismatch")
+			assert.Equal(t, tc.expectFlowKey, fcReq.FlowKey(), "FlowKey() mismatch")
+			assert.Zero(t, fcReq.InitialEffectiveTTL(), "InitialEffectiveTTL() should be zero")
+		})
+	}
+}
+func TestFlowControlAdmissionController_Admit(t *testing.T) {
+	t.Parallel()
+	ctx := logutil.NewTestLoggerIntoContext(context.Background())
+	candidatePods := []backendmetrics.PodMetrics{}
+
+	reqCtx := &handlers.RequestContext{
+		SchedulingRequest: &schedulingtypes.LLMRequest{RequestId: "test-req"},
+	}
+
+	testCases := []struct {
+		name            string
+		priority        int
+		isSaturated     bool
+		fcOutcome       fctypes.QueueOutcome
+		fcErr           error
+		expectErr       bool
+		expectErrCode   string
+		expectErrSubstr string
+		expectFCSkipped bool
+	}{
+		{
+			name:            "sheddable_saturated_reject",
+			priority:        -1,
+			isSaturated:     true,
+			expectErr:       true,
+			expectErrCode:   errutil.InferencePoolResourceExhausted,
+			expectErrSubstr: "system saturated, sheddable request dropped",
+			expectFCSkipped: true,
+		},
+		{
+			name:        "sheddable_not_saturated_dispatch",
+			priority:    -1,
+			isSaturated: false,
+			fcOutcome:   fctypes.QueueOutcomeDispatched,
+			expectErr:   false,
+		},
+		{
+			name:        "non_sheddable_saturated_dispatch",
+			priority:    0,
+			isSaturated: true,
+			fcOutcome:   fctypes.QueueOutcomeDispatched,
+			expectErr:   false,
+		},
+		{
+			name:            "fc_reject_capacity",
+			priority:        0,
+			fcOutcome:       fctypes.QueueOutcomeRejectedCapacity,
+			expectErr:       true,
+			expectErrCode:   errutil.InferencePoolResourceExhausted,
+			expectErrSubstr: "request rejected by flow control",
+		},
+		{
+			name:            "fc_evict_ttl",
+			priority:        0,
+			fcOutcome:       fctypes.QueueOutcomeEvictedTTL,
+			fcErr:           errors.New("timeout"),
+			expectErr:       true,
+			expectErrCode:   errutil.ServiceUnavailable,
+			expectErrSubstr: "request timed out in queue: timeout",
+		},
+		{
+			name:            "fc_evict_context_cancelled",
+			priority:        0,
+			fcOutcome:       fctypes.QueueOutcomeEvictedContextCancelled,
+			expectErr:       true,
+			expectErrCode:   errutil.ServiceUnavailable,
+			expectErrSubstr: "client disconnected",
+		},
+		{
+			name:            "fc_reject_other",
+			priority:        0,
+			fcOutcome:       fctypes.QueueOutcomeRejectedOther,
+			expectErr:       true,
+			expectErrCode:   errutil.Internal,
+			expectErrSubstr: "internal flow control error",
+		},
+		{
+			name:            "fc_evict_other",
+			priority:        0,
+			fcOutcome:       fctypes.QueueOutcomeEvictedOther,
+			fcErr:           errors.New("internal error"),
+			expectErr:       true,
+			expectErrCode:   errutil.Internal,
+			expectErrSubstr: "internal flow control error: internal error",
+		},
+		{
+			name:            "fc_unhandled_outcome",
+			priority:        0,
+			fcOutcome:       fctypes.QueueOutcomeNotYetFinalized,
+			expectErr:       true,
+			expectErrCode:   errutil.Internal,
+			expectErrSubstr: "unhandled flow control outcome",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			sd := &mockSaturationDetector{isSaturated: tc.isSaturated}
+			fc := &mockFlowController{outcome: tc.fcOutcome, err: tc.fcErr}
+			ac := NewFlowControlAdmissionController(sd, fc)
+
+			err := ac.Admit(ctx, reqCtx, candidatePods, tc.priority)
+
+			if tc.expectFCSkipped {
+				assert.False(t, fc.called, "FlowController should not have been called for scenario: %s", tc.name)
+			} else {
+				assert.True(t, fc.called, "FlowController should have been called for scenario: %s", tc.name)
+			}
+
+			if !tc.expectErr {
+				assert.NoError(t, err, "Admit() returned an unexpected error for scenario: %s", tc.name)
+			} else {
+				require.Error(t, err, "Admit() should have returned an error for scenario: %s", tc.name)
+				var e errutil.Error
+				if assert.ErrorAs(t, err, &e, "error should be of type errutil.Error") {
+					assert.Equal(t, tc.expectErrCode, e.Code, "incorrect error code for scenario: %s", tc.name)
+					assert.Contains(t, e.Msg, tc.expectErrSubstr, "incorrect error message substring for scenario: %s", tc.name)
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/epp/requestcontrol/director_test.go b/pkg/epp/requestcontrol/director_test.go
index a0cb7c3..777e287 100644
--- a/pkg/epp/requestcontrol/director_test.go
+++ b/pkg/epp/requestcontrol/director_test.go
@@ -50,12 +50,17 @@ import (
 
 // --- Mocks ---
 
-type mockSaturationDetector struct {
-	isSaturated bool
+type mockAdmissionController struct {
+	admitErr error
 }
 
-func (m *mockSaturationDetector) IsSaturated(_ context.Context, _ []backendmetrics.PodMetrics) bool {
-	return m.isSaturated
+func (m *mockAdmissionController) Admit(
+	_ context.Context,
+	_ *handlers.RequestContext,
+	_ []backendmetrics.PodMetrics,
+	_ int,
+) error {
+	return m.admitErr
 }
 
 type mockScheduler struct {
@@ -71,8 +76,12 @@ type mockDatastore struct {
 	pods []backendmetrics.PodMetrics
 }
 
-func (ds *mockDatastore) PoolGet() (*v1.InferencePool, error)                { return nil, nil }
-func (ds *mockDatastore) ObjectiveGet(_ string) *v1alpha2.InferenceObjective { return nil }
+func (ds *mockDatastore) PoolGet() (*v1.InferencePool, error) {
+	return nil, nil
+}
+func (ds *mockDatastore) ObjectiveGet(_ string) *v1alpha2.InferenceObjective {
+	return nil
+}
 func (ds *mockDatastore) PodList(predicate func(backendmetrics.PodMetrics) bool) []backendmetrics.PodMetrics {
 	res := []backendmetrics.PodMetrics{}
 	for _, pod := range ds.pods {
@@ -187,23 +196,23 @@ func TestDirector_HandleRequest(t *testing.T) {
 	}
 
 	tests := []struct {
-		name                   string
-		reqBodyMap             map[string]any
-		mockSaturationDetector *mockSaturationDetector
-		inferenceObjectiveName string
-		schedulerMockSetup     func(m *mockScheduler)
-		wantErrCode            string                   // Expected errutil code string
-		wantReqCtx             *handlers.RequestContext // Fields to check in the returned RequestContext
-		wantMutatedBodyModel   string                   // Expected model in reqCtx.Request.Body after PostDispatch
-		targetModelName        string                   // Expected model name after target model resolution
+		name                    string
+		reqBodyMap              map[string]any
+		mockAdmissionController *mockAdmissionController
+		inferenceObjectiveName  string
+		schedulerMockSetup      func(m *mockScheduler)
+		wantErrCode             string                   // Expected errutil code string
+		wantReqCtx              *handlers.RequestContext // Fields to check in the returned RequestContext
+		wantMutatedBodyModel    string                   // Expected model in reqCtx.Request.Body after PostDispatch
+		targetModelName         string                   // Expected model name after target model resolution
 	}{
 		{
-			name: "successful completions request (critical, saturation ignored)",
+			name: "successful completions request",
 			reqBodyMap: map[string]any{
 				"model":  model,
 				"prompt": "critical prompt",
 			},
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
@@ -221,7 +230,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			targetModelName:        model,
 		},
 		{
-			name: "successful chat completions request (default critical, saturation ignored)",
+			name: "successful chat completions request",
 			reqBodyMap: map[string]any{
 				"model": model,
 				"messages": []any{
@@ -231,7 +240,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 					},
 				},
 			},
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
@@ -247,7 +256,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			targetModelName:      model,
 		},
 		{
-			name: "successful chat completions request with multiple messages (critical, saturation ignored)",
+			name: "successful chat completions request with multiple messages",
 			reqBodyMap: map[string]any{
 				"model": model,
 				"messages": []any{
@@ -261,6 +270,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 					},
 				},
 			},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
@@ -277,36 +287,13 @@ func TestDirector_HandleRequest(t *testing.T) {
 			inferenceObjectiveName: objectiveName,
 			targetModelName:        model,
 		},
-		{
-			name: "successful completions request (sheddable, not saturated)",
-			reqBodyMap: map[string]any{
-				"model":  modelSheddable,
-				"prompt": "sheddable prompt",
-			},
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
-			schedulerMockSetup: func(m *mockScheduler) {
-				m.scheduleResults = defaultSuccessfulScheduleResults
-			},
-			wantReqCtx: &handlers.RequestContext{
-				ObjectiveKey:    objectiveNameSheddable,
-				TargetModelName: modelSheddable,
-				TargetPod: &backend.Pod{
-					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
-					Address:        "192.168.1.100",
-				},
-				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
-			},
-			wantMutatedBodyModel:   modelSheddable,
-			inferenceObjectiveName: objectiveNameSheddable,
-			targetModelName:        modelSheddable,
-		},
 		{
 			name: "successful request with target model resolution",
 			reqBodyMap: map[string]any{
 				"model":  modelWithResolvedTarget,
 				"prompt": "prompt for target resolution",
 			},
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
@@ -342,28 +329,27 @@ func TestDirector_HandleRequest(t *testing.T) {
 				"model":  "food-review-1",
 				"prompt": "test prompt",
 			},
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
-			inferenceObjectiveName: "food-review-1",
-			targetModelName:        "food-review-1",
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
+			inferenceObjectiveName:  "food-review-1",
+			targetModelName:         "food-review-1",
 		},
 		{
 
-			name: "request dropped (sheddable, saturated)",
+			name: "request rejected by admission controller",
 			reqBodyMap: map[string]any{
 				"model":  modelSheddable,
 				"prompt": "sheddable prompt",
 			},
-			inferenceObjectiveName: objectiveNameSheddable,
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
-			wantErrCode:            errutil.InferencePoolResourceExhausted,
+			inferenceObjectiveName:  objectiveNameSheddable,
+			mockAdmissionController: &mockAdmissionController{admitErr: errutil.Error{Code: errutil.InferencePoolResourceExhausted, Msg: "simulated admission rejection"}},
+			wantErrCode:             errutil.InferencePoolResourceExhausted,
 		},
 		{
-			name:                   "model not found, expect err",
-			reqBodyMap:             map[string]any{"prompt": "p"},
-			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
-			wantErrCode:            errutil.BadRequest,
+			name:                    "model not found, expect err",
+			reqBodyMap:              map[string]any{"prompt": "p"},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
+			wantErrCode:             errutil.BadRequest,
 		},
-
 		{
 			name:        "prompt or messages not found, expect err",
 			reqBodyMap:  map[string]any{"model": model},
@@ -383,6 +369,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 				"model":  model,
 				"prompt": "prompt that causes scheduler error",
 			},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleErr = errors.New("simulated scheduler failure")
 			},
@@ -395,6 +382,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 				"model":  model,
 				"prompt": "prompt for nil,nil scheduler return",
 			},
+			mockAdmissionController: &mockAdmissionController{admitErr: nil},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = nil
 				m.scheduleErr = nil
@@ -410,7 +398,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			if test.schedulerMockSetup != nil {
 				test.schedulerMockSetup(mockSched)
 			}
-			director := NewDirectorWithConfig(ds, mockSched, test.mockSaturationDetector, NewConfig())
+			director := NewDirectorWithConfig(ds, mockSched, test.mockAdmissionController, NewConfig())
 
 			reqCtx := &handlers.RequestContext{
 				Request: &handlers.Request{
@@ -529,7 +517,7 @@ func TestGetCandidatePodsForScheduling(t *testing.T) {
 	ds := &mockDatastore{pods: testInput}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			director := NewDirectorWithConfig(ds, &mockScheduler{}, &mockSaturationDetector{}, NewConfig())
+			director := NewDirectorWithConfig(ds, &mockScheduler{}, &mockAdmissionController{}, NewConfig())
 
 			got := director.getCandidatePodsForScheduling(context.Background(), test.metadata)
 
@@ -598,7 +586,7 @@ func TestDirector_HandleResponse(t *testing.T) {
 	ctx := logutil.NewTestLoggerIntoContext(context.Background())
 	ds := datastore.NewDatastore(t.Context(), nil)
 	mockSched := &mockScheduler{}
-	director := NewDirectorWithConfig(ds, mockSched, nil, NewConfig().WithPostResponsePlugins(pr1))
+	director := NewDirectorWithConfig(ds, mockSched, &mockAdmissionController{}, NewConfig().WithPostResponsePlugins(pr1))
 
 	reqCtx := &handlers.RequestContext{
 		Request: &handlers.Request{
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer_test.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer_test.go
index 6d4fcc5..c35af8e 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer_test.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/indexer_test.go
@@ -46,3 +46,63 @@ func TestIndexer_AddAndGet(t *testing.T) {
 	servers = i.Get(BlockHash(4))
 	assert.Empty(t, servers, "Cache should not contain non-existent hash")
 }
+
+func TestIndexer_RemovePodAndEviction(t *testing.T) {
+	const indexerSize = 10
+
+	i := newIndexer(context.Background(), indexerSize)
+
+	server1 := ServerID{Namespace: "default", Name: "server1"}
+	server2 := ServerID{Namespace: "default", Name: "server2"}
+
+	// Add indexerSize hashes to both servers
+	var hashes []BlockHash
+	for j := 0; j < indexerSize; j++ {
+		h := BlockHash(j)
+		hashes = append(hashes, h)
+		i.Add([]BlockHash{h}, server1)
+		i.Add([]BlockHash{h}, server2)
+	}
+
+	// Ensure all entries are added
+	assert.Equal(t, indexerSize, i.podToLRU[server1].Len(), "server1 should have 10 entries")
+	assert.Equal(t, indexerSize, i.podToLRU[server2].Len(), "server2 should have 10 entries")
+
+	// Ensure each hash in hashToPods maps to both server1 and server2
+	for _, h := range hashes {
+		pods := i.hashToPods[h]
+		assert.Len(t, pods, 2, "Each hash should be associated with exactly 2 pods")
+		assert.Contains(t, pods, server1, "hash should be associated with server1")
+		assert.Contains(t, pods, server2, "hash should be associated with server2")
+	}
+
+	// Add indexerSize hash to server1 → should evict BlockHash(0)
+	evictedHash := BlockHash(0)
+	newHash := BlockHash(indexerSize)
+	i.Add([]BlockHash{newHash}, server1)
+
+	// server1 LRU should still be at max capacity
+	assert.Equal(t, indexerSize, i.podToLRU[server1].Len(), "server1 LRU should maintain max size")
+
+	// BlockHash(0) should no longer have server1 in hashToPods
+	pods := i.Get(evictedHash)
+	assert.NotContains(t, pods, server1, "server1 should be evicted from hashToPods for hash 0")
+	assert.Contains(t, pods, server2, "server2 should still have hash 0")
+
+	// Remove server2
+	i.RemovePod(server2)
+
+	// hashToPods for hash 0 should now be empty
+	pods = i.Get(evictedHash)
+	assert.NotContains(t, pods, server2, "server2 should be removed from hash 0")
+	assert.Empty(t, pods, "hash 0 should have no pods after both eviction and removal")
+
+	// All remaining hashes should map only to server1
+	for hash, pods := range i.hashToPods {
+		assert.Len(t, pods, 1, "hash %v should have only 1 pod after server2 removal", hash)
+		assert.Contains(t, pods, server1, "hash %v should only contain server1", hash)
+	}
+
+	// Ensure hashToPods contains exactly indexerSize hashes (post-eviction and server2 removal)
+	assert.Len(t, i.hashToPods, indexerSize, "hashToPods should contain %d hashes after cleanup", indexerSize)
+}
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
index 54a00ab..7d4e1a4 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin_test.go
@@ -19,7 +19,6 @@ package prefix
 import (
 	"context"
 	"fmt"
-	"math"
 	"math/rand"
 	"strings"
 	"testing"
@@ -357,49 +356,44 @@ func BenchmarkPrefixPluginStress(b *testing.B) {
 	plugin := New(context.Background(), config)
 	types.NewCycleState()
 	var promptLen []int
-	for i := 1; i <= 1024; i++ {
+	for i := 1; i <= 1024; {
 		promptLen = append(promptLen, i)
+		i += 10
 	}
 	promptLen = append(promptLen, 2048, 4096, 8192, 10000, 20000, 50000)
 
-	for _, i := range promptLen {
-		// Generate increasing-length random prompts
-		prompt := randomPrompt(4 + i)
-		pod := &types.PodMetrics{
-			Pod: &backend.Pod{
-				NamespacedName: k8stypes.NamespacedName{
-					Name: fmt.Sprintf("random-pod-%d", i),
+	for i, v := range promptLen {
+		b.Run(fmt.Sprintf("messages_%d_length_%d", i, v), func(b *testing.B) {
+			// Generate increasing-length random prompts
+			prompt := randomPrompt(4 + v)
+			pod := &types.PodMetrics{
+				Pod: &backend.Pod{
+					NamespacedName: k8stypes.NamespacedName{
+						Name: fmt.Sprintf("random-pod-%d", v),
+					},
 				},
-			},
-		}
-
-		pods := []types.Pod{pod}
-		req := &types.LLMRequest{
-			RequestId:   uuid.NewString(),
-			TargetModel: "model-stress",
-			Body: &types.LLMRequestBody{
-				Completions: &types.CompletionsRequest{
-					Prompt: prompt,
+			}
+
+			pods := []types.Pod{pod}
+			req := &types.LLMRequest{
+				RequestId:   uuid.NewString(),
+				TargetModel: "model-stress",
+				Body: &types.LLMRequestBody{
+					Completions: &types.CompletionsRequest{
+						Prompt: prompt,
+					},
 				},
-			},
-		}
-
-		// First cycle: simulate scheduling and insert prefix info into the cache
-		plugin.Score(context.Background(), types.NewCycleState(), req, pods)
-		schedulingResult := &types.SchedulingResult{
-			PrimaryProfileName: "default",
-			ProfileResults: map[string]*types.ProfileRunResult{
-				"default": {TargetPods: []types.Pod{pod}},
-			},
-		}
-		plugin.PreRequest(context.Background(), req, schedulingResult, 0)
-		plugin.wg.Wait()
-
-		// Second cycle: validate internal state
-		state, err := plugins.ReadPluginStateKey[*SchedulingContextState](plugin.pluginState, req.RequestId, plugins.StateKey(plugin.TypedName().String()))
-		assert.NoError(b, err)
-		expectedHashes := int(math.Min(float64(maxPrefixBlocks), float64(len(req.Body.Completions.Prompt)/blockSize)))
-		assert.Equal(b, expectedHashes, len(state.PrefixHashes), "number of hashes is incorrect")
+			}
+
+			b.ResetTimer()
+			// Benchmark the scoring operation
+			scores := plugin.Score(context.Background(), types.NewCycleState(), req, pods)
+			_ = scores // Use the result to prevent optimization
+
+			// Clean up state for next iteration
+			plugin.pluginState.Delete(req.RequestId)
+		})
+
 	}
 }
 
@@ -422,7 +416,6 @@ func BenchmarkPrefixPluginChatCompletionsStress(b *testing.B) {
 		MaxPrefixBlocksToMatch: maxPrefixBlocks,
 		LRUCapacityPerServer:   DefaultLRUCapacityPerServer,
 	}
-
 	plugin := New(context.Background(), config)
 
 	// Test scenarios: varying number of messages and message lengths
@@ -476,7 +469,7 @@ func BenchmarkPrefixPluginChatCompletionsStress(b *testing.B) {
 			b.ResetTimer()
 			for i := 0; i < b.N; i++ {
 				// Benchmark the scoring operation
-				scores := plugin.Score(context.Background(), nil, req, pods)
+				scores := plugin.Score(context.Background(), types.NewCycleState(), req, pods)
 				_ = scores // Use the result to prevent optimization
 
 				// Clean up state for next iteration
diff --git a/pkg/epp/util/request/body_test.go b/pkg/epp/util/request/body_test.go
index 64ab6de..51389e5 100644
--- a/pkg/epp/util/request/body_test.go
+++ b/pkg/epp/util/request/body_test.go
@@ -225,6 +225,44 @@ func TestExtractRequestData(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "completions request with cache_salt",
+			body: map[string]any{
+				"model":      "test",
+				"prompt":     "test prompt",
+				"cache_salt": "Z3V2bmV3aGxza3ZubGFoZ3Zud3V3ZWZ2bmd0b3V2bnZmc2xpZ3RoZ2x2aQ==",
+			},
+			want: &types.LLMRequestBody{
+				Completions: &types.CompletionsRequest{
+					Prompt:    "test prompt",
+					CacheSalt: "Z3V2bmV3aGxza3ZubGFoZ3Zud3V3ZWZ2bmd0b3V2bnZmc2xpZ3RoZ2x2aQ==",
+				},
+			},
+		},
+		{
+			name: "chat completions request with cache_salt",
+			body: map[string]any{
+				"model": "test",
+				"messages": []any{
+					map[string]any{
+						"role": "system", "content": "this is a system message",
+					},
+					map[string]any{
+						"role": "user", "content": "hello",
+					},
+				},
+				"cache_salt": "Z3V2bmV3aGxza3ZubGFoZ3Zud3V3ZWZ2bmd0b3V2bnZmc2xpZ3RoZ2x2aQ==",
+			},
+			want: &types.LLMRequestBody{
+				ChatCompletions: &types.ChatCompletionsRequest{
+					Messages: []types.Message{
+						{Role: "system", Content: "this is a system message"},
+						{Role: "user", Content: "hello"},
+					},
+					CacheSalt: "Z3V2bmV3aGxza3ZubGFoZ3Zud3V3ZWZ2bmd0b3V2bnZmc2xpZ3RoZ2x2aQ==",
+				},
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/test/integration/epp/hermetic_test.go b/test/integration/epp/hermetic_test.go
index 3dc42f8..c2e100f 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -1180,7 +1180,8 @@ func BeforeSuite() func() {
 	}
 	detector := saturationdetector.NewDetector(sdConfig, logger.WithName("saturation-detector"))
 	serverRunner.SaturationDetector = detector
-	serverRunner.Director = requestcontrol.NewDirectorWithConfig(serverRunner.Datastore, scheduler, detector, requestcontrol.NewConfig())
+	admissionController := requestcontrol.NewLegacyAdmissionController(detector)
+	serverRunner.Director = requestcontrol.NewDirectorWithConfig(serverRunner.Datastore, scheduler, admissionController, requestcontrol.NewConfig())
 	serverRunner.SecureServing = false
 
 	if err := serverRunner.SetupWithManager(context.Background(), mgr); err != nil {
diff --git a/test/utils/handle.go b/test/utils/handle.go
index 4a29dda..273539f 100644
--- a/test/utils/handle.go
+++ b/test/utils/handle.go
@@ -19,6 +19,7 @@ package utils
 import (
 	"context"
 
+	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 )
 
@@ -33,6 +34,10 @@ func (h *testHandle) Context() context.Context {
 	return h.ctx
 }
 
+func (h *testHandle) PodList(predicate func(backendmetrics.PodMetrics) bool) []backendmetrics.PodMetrics {
+	return []backendmetrics.PodMetrics{}
+}
+
 type testHandlePlugins struct {
 	plugins map[string]plugins.Plugin
 }