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

Issue:
EndpointPicker: Duplicate flags in runner.go in EPP
There seems to be duplicate flags in [runner.go](https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/cmd/epp/runner/runner.go), that is, same flags can be pass arguments to binary and set as environment variables:

Passed as arguments, for example modelServerMetricsPath:
https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/cmd/epp/runner/runner.go#L152

Can also be set as environment variables:
https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/cmd/epp/runner/runner.go#L192


Do we need them to be configurable in 2 places? Or should we clean them up?

Hints from issue comments:
@ahg-g: I would remove the env vars, not need to have both@nirrozenbaum: > I would remove the env vars, not need to have both

I agree.

@danehans can you chime in? 
If I remember correctly that was a PR that was merged recently and you were reviewing it.@ahg-g: I recommend a pattern where env vars are used for temporary "feature flags" only and not for permanent flags.

Solution patch:
diff --git a/.custom-gcl.yml b/.custom-gcl.yml
new file mode 100644
index 0000000..fc27c17
--- /dev/null
+++ b/.custom-gcl.yml
@@ -0,0 +1,6 @@
+version: v2.3.1
+name: golangci-kube-api-linter
+destination: ./bin
+plugins:
+- module: 'sigs.k8s.io/kube-api-linter'
+  version: 'v0.0.0-20250808120943-48643eb2563d' # Pin to a commit while there's no tag
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..d21c25e
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,56 @@
+<!--  Thanks for sending a pull request! Here are some tips for you:
+
+1. If this is your first time contributing to Gateway API Inference Extension, please read our
+   developer guide (https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/docs/dev.md)
+   and our community page (https://gateway-api-inference-extension.sigs.k8s.io/contributing/).
+2. If this is your first time contributing to a Kubernetes project, please read
+   our contributor guidelines:
+   https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution
+3. Please label this pull request according to what type of issue you are
+   addressing, especially if this is a release targeted pull request. For
+   reference on required PR/issue labels, read here:
+   https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
+4. If you want *faster* PR reviews, read how:
+   https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
+5. If the PR is unfinished, see how to mark it:
+   https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
+6. If this PR is a new design proposal, please review existing design docs for guidance:
+   https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/docs/proposals
+-->
+
+**What type of PR is this?**
+<!--
+Add one of the following kinds:
+/kind bug
+/kind cleanup
+/kind documentation
+/kind feature
+/kind gep
+/kind test
+
+Optionally add one or more of the following kinds if applicable:
+/kind deprecation
+/kind failing-test
+/kind flake
+/kind regression
+/area conformance-test
+/area conformance-machinery
+-->
+
+**What this PR does / why we need it**:
+
+**Which issue(s) this PR fixes**:
+<!--
+*Automatically closes linked issue when PR is merged.
+Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
+-->
+Fixes #
+
+**Does this PR introduce a user-facing change?**:
+<!--
+If no, just write "NONE" in the release-note block below.
+If yes, please enter a release note below:
+-->
+```release-note
+
+```
diff --git a/.github/workflows/kal.yml b/.github/workflows/kal.yml
new file mode 100644
index 0000000..618256e
--- /dev/null
+++ b/.github/workflows/kal.yml
@@ -0,0 +1,28 @@
+name: PR golangci-lint
+
+on:
+  pull_request:
+    types: [opened, edited, synchronize, reopened]
+
+# Remove all permissions from GITHUB_TOKEN except metadata.
+permissions: {}
+
+jobs:
+  golangci:
+    name: kube-api-lint
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+        name: Checkout code
+        with:
+          persist-credentials: false
+      - name: Set up Go
+        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # tag=v5.5.0
+      - name: Install Golang CI Lint
+        run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.2.1
+      - name: Build KAL
+        run: golangci-lint custom
+      - name: run api linter
+        run: ./bin/golangci-kube-api-linter run -c ./.golangci-kal.yml ./...
diff --git a/.gitignore b/.gitignore
index 4442b65..82afc2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,6 @@ go.work.sum
 
 # generated docs
 site
+
+# MacOS generated files
+**/.DS_Store
diff --git a/.golangci-kal.yml b/.golangci-kal.yml
new file mode 100644
index 0000000..a5d853b
--- /dev/null
+++ b/.golangci-kal.yml
@@ -0,0 +1,46 @@
+version: "2"
+linters:
+  default: none
+  enable:
+    - kubeapilinter
+  settings:
+    custom:
+      kubeapilinter:
+        type: module
+        description: Kube API LInter lints Kube like APIs based on API conventions and best practices.
+        settings:
+          linters:
+            enable:
+              - "*"
+            disable:
+              - "commentstart"
+          lintersConfig:
+            conflictingmarkers:
+              conflicts:
+              - name: "default_vs_required"
+                sets:
+                  - ["default", "kubebuilder:default"]
+                  - ["required", "kubebuilder:validation:Required", "k8s:required"]
+                description: "A field with a default value cannot be required"
+            conditions:
+              isFirstField: Warn # Require conditions to be the first field in the status struct.
+              usePatchStrategy: Forbid # Forbid patchStrategy markers on the Conditions field.
+              useProtobuf: Forbid # We don't use protobuf, so protobuf tags are not required.
+            optionalfields:
+              pointers:
+                preference: WhenRequired # Always | WhenRequired # Whether to always require pointers, or only when required. Defaults to `Always`.
+                policy: SuggestFix # SuggestFix | Warn # The policy for pointers in optional fields. Defaults to `SuggestFix`. 
+              omitempty:
+                policy: SuggestFix # SuggestFix | Warn | Ignore # The policy for omitempty in optional fields. Defaults to `SuggestFix`.
+  exclusions:
+    generated: strict
+    paths:
+      - conformance/
+    paths-except:
+      - api/
+issues:
+  max-issues-per-linter: 0
+  max-same-issues: 0
+run:
+  timeout: 5m
+  tests: false
diff --git a/README.md b/README.md
index 8212c73..51aaf28 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ For deeper insights and more advanced concepts, refer to our [proposals](/docs/p
 
 ## Technical Overview
 
-This extension upgrades an [ext-proc](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_proc_filter) capable proxy or gateway - such as Envoy Gateway, kGateway, or the GKE Gateway - to become an **[inference gateway]** - supporting inference platform teams self-hosting Generative Models (with a current focus on large language models) on Kubernetes. This integration makes it easy to expose and control access to your local [OpenAI-compatible chat completion endpoints](https://platform.openai.com/docs/api-reference/chat) to other workloads on or off cluster, or to integrate your self-hosted models alongside model-as-a-service providers in a higher level **AI Gateway** like LiteLLM, Solo AI Gateway, or Apigee.
+This extension upgrades an [ext-proc](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_proc_filter) capable proxy or gateway - such as Envoy Gateway, kgateway, or the GKE Gateway - to become an **[inference gateway]** - supporting inference platform teams self-hosting Generative Models (with a current focus on large language models) on Kubernetes. This integration makes it easy to expose and control access to your local [OpenAI-compatible chat completion endpoints](https://platform.openai.com/docs/api-reference/chat) to other workloads on or off cluster, or to integrate your self-hosted models alongside model-as-a-service providers in a higher level **AI Gateway** like LiteLLM, Solo AI Gateway, or Apigee.
 
 The Inference Gateway:
 
diff --git a/api/v1/inferencepool_types.go b/api/v1/inferencepool_types.go
index 98f45d5..ed8e0e1 100644
--- a/api/v1/inferencepool_types.go
+++ b/api/v1/inferencepool_types.go
@@ -25,19 +25,24 @@ import (
 // +kubebuilder:object:root=true
 // TODO: change the annotation once it gets officially approved
 // +kubebuilder:metadata:annotations="api-approved.kubernetes.io=unapproved, experimental-only"
+// +kubebuilder:resource:shortName=infpool
 // +kubebuilder:subresource:status
 // +kubebuilder:storageversion
 // +genclient
 type InferencePool struct {
-	metav1.TypeMeta   `json:",inline"`
+	metav1.TypeMeta `json:",inline"`
+
+	// +optional
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
-	Spec InferencePoolSpec `json:"spec,omitempty"`
+	// +required
+	Spec InferencePoolSpec `json:"spec,omitzero"`
 
 	// Status defines the observed state of InferencePool.
 	//
 	// +kubebuilder:default={parent: {{parentRef: {kind: "Status", name: "default"}, conditions: {{type: "Accepted", status: "Unknown", reason: "Pending", message: "Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}}}}
-	Status InferencePoolStatus `json:"status,omitempty"`
+	// +optional
+	Status InferencePoolStatus `json:"status,omitzero"`
 }
 
 // InferencePoolList contains a list of InferencePool.
@@ -51,55 +56,42 @@ type InferencePoolList struct {
 
 // InferencePoolSpec defines the desired state of InferencePool
 type InferencePoolSpec struct {
-	// Selector defines a map of labels to watch model server Pods
-	// that should be included in the InferencePool.
-	// In some cases, implementations may translate this field to a Service selector, so this matches the simple
-	// map used for Service selectors instead of the full Kubernetes LabelSelector type.
-	// If specified, it will be applied to match the model server pods in the same namespace as the InferencePool.
-	// Cross namesoace selector is not supported.
+	// Selector determines which Pods are members of this inference pool.
+	// It matches Pods by their labels only within the same namespace; cross-namespace
+	// selection is not supported.
 	//
-	// +kubebuilder:validation:Required
-	Selector map[LabelKey]LabelValue `json:"selector"`
-
-	// TargetPortNumber defines the port number to access the selected model server Pods.
-	// The number must be in the range 1 to 65535.
+	// The structure of this LabelSelector is intentionally simple to be compatible
+	// with Kubernetes Service selectors, as some implementations may translate
+	// this configuration into a Service resource.
 	//
-	// +kubebuilder:validation:Minimum=1
-	// +kubebuilder:validation:Maximum=65535
-	// +kubebuilder:validation:Required
-	TargetPortNumber int32 `json:"targetPortNumber"`
+	// +required
+	Selector LabelSelector `json:"selector,omitempty,omitzero"`
 
-	// EndpointPickerConfig specifies the configuration needed by the proxy to discover and connect to the endpoint
-	// picker service that picks endpoints for the requests routed to this pool.
-	EndpointPickerConfig `json:",inline"`
-}
+	// TargetPorts defines a list of ports that are exposed by this InferencePool.
+	// Currently, the list may only include a single port definition.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=1
+	// +listType=map
+	// +listMapKey=number
+	// +required
+	TargetPorts []Port `json:"targetPorts,omitempty"`
 
-// EndpointPickerConfig specifies the configuration needed by the proxy to discover and connect to the endpoint picker extension.
-// This type is intended to be a union of mutually exclusive configuration options that we may add in the future.
-type EndpointPickerConfig struct {
 	// Extension configures an endpoint picker as an extension service.
+	// +required
+	ExtensionRef Extension `json:"extensionRef,omitempty,omitzero"`
+}
+
+// Port defines the network port that will be exposed by this InferencePool.
+type Port struct {
+	// Number defines the port number to access the selected model server Pods.
+	// The number must be in the range 1 to 65535.
 	//
-	// +kubebuilder:validation:Required
-	ExtensionRef *Extension `json:"extensionRef,omitempty"`
+	// +required
+	Number PortNumber `json:"number,omitempty"`
 }
 
 // Extension specifies how to configure an extension that runs the endpoint picker.
 type Extension struct {
-	// Reference is a reference to a service extension. When ExtensionReference is invalid,
-	// a 5XX status code MUST be returned for the request that would have otherwise been routed
-	// to the invalid backend.
-	ExtensionReference `json:",inline"`
-
-	// ExtensionConnection configures the connection between the Gateway and the extension.
-	ExtensionConnection `json:",inline"`
-}
-
-// ExtensionReference is a reference to the extension.
-//
-// If a reference is invalid, the implementation MUST update the `ResolvedRefs`
-// Condition on the InferencePool's status to `status: False`. A 5XX status code MUST be returned
-// for the request that would have otherwise been routed to the invalid backend.
-type ExtensionReference struct {
 	// Group is the group of the referent.
 	// The default value is "", representing the Core API group.
 	//
@@ -119,29 +111,26 @@ type ExtensionReference struct {
 	//
 	// +optional
 	// +kubebuilder:default=Service
-	Kind *Kind `json:"kind,omitempty"`
+	Kind Kind `json:"kind,omitempty"`
 
 	// Name is the name of the referent.
 	//
-	// +kubebuilder:validation:Required
-	Name ObjectName `json:"name"`
+	// +required
+	Name ObjectName `json:"name,omitempty"`
 
 	// The port number on the service running the extension. When unspecified,
 	// implementations SHOULD infer a default value of 9002 when the Kind is
 	// Service.
 	//
 	// +optional
-	PortNumber *PortNumber `json:"portNumber,omitempty"`
-}
+	PortNumber PortNumber `json:"portNumber,omitempty"`
 
-// ExtensionConnection encapsulates options that configures the connection to the extension.
-type ExtensionConnection struct {
 	// Configures how the gateway handles the case when the extension is not responsive.
 	// Defaults to failClose.
 	//
 	// +optional
 	// +kubebuilder:default="FailClose"
-	FailureMode *ExtensionFailureMode `json:"failureMode"`
+	FailureMode ExtensionFailureMode `json:"failureMode,omitempty"`
 }
 
 // ExtensionFailureMode defines the options for how the gateway handles the case when the extension is not
@@ -157,6 +146,7 @@ const (
 )
 
 // InferencePoolStatus defines the observed state of InferencePool.
+// +kubebuilder:validation:MinProperties=1
 type InferencePoolStatus struct {
 	// Parents is a list of parent resources (usually Gateways) that are
 	// associated with the InferencePool, and the status of the InferencePool with respect to
@@ -171,14 +161,13 @@ type InferencePoolStatus struct {
 	//    and no other parents exist.
 	//
 	// +kubebuilder:validation:MaxItems=32
+	// +optional
+	// +listType=atomic
 	Parents []PoolStatus `json:"parent,omitempty"`
 }
 
 // PoolStatus defines the observed state of InferencePool from a Gateway.
 type PoolStatus struct {
-	// GatewayRef indicates the gateway that observed state of InferencePool.
-	GatewayRef ParentGatewayReference `json:"parentRef"`
-
 	// Conditions track the state of the InferencePool.
 	//
 	// Known condition types are:
@@ -192,6 +181,10 @@ type PoolStatus struct {
 	// +kubebuilder:validation:MaxItems=8
 	// +kubebuilder:default={{type: "Accepted", status: "Unknown", reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}
 	Conditions []metav1.Condition `json:"conditions,omitempty"`
+
+	// GatewayRef indicates the gateway that observed state of InferencePool.
+	// +required
+	GatewayRef ParentGatewayReference `json:"parentRef,omitzero"`
 }
 
 // InferencePoolConditionType is a type of condition for the InferencePool
@@ -261,7 +254,7 @@ const (
 	InferencePoolReasonResolvedRefs InferencePoolReason = "ResolvedRefs"
 
 	// This reason is used with the "ResolvedRefs" condition when the
-	// ExtensionRef is invalid in some way. This can include an unsupported kind
+	// Extension is invalid in some way. This can include an unsupported kind
 	// or API group, or a reference to a resource that can not be found.
 	InferencePoolReasonInvalidExtensionRef InferencePoolReason = "InvalidExtensionRef"
 )
@@ -273,21 +266,22 @@ type ParentGatewayReference struct {
 	//
 	// +optional
 	// +kubebuilder:default="gateway.networking.k8s.io"
-	Group *Group `json:"group"`
+	Group *Group `json:"group,omitempty"`
 
 	// Kind is kind of the referent. For example "Gateway".
 	//
 	// +optional
 	// +kubebuilder:default=Gateway
-	Kind *Kind `json:"kind"`
+	Kind Kind `json:"kind,omitempty"`
 
 	// Name is the name of the referent.
-	Name ObjectName `json:"name"`
+	// +required
+	Name ObjectName `json:"name,omitempty"`
 
 	// Namespace is the namespace of the referent.  If not present,
 	// the namespace of the referent is assumed to be the same as
 	// the namespace of the referring object.
 	//
 	// +optional
-	Namespace *Namespace `json:"namespace,omitempty"`
+	Namespace Namespace `json:"namespace,omitempty"`
 }
diff --git a/api/v1/shared_types.go b/api/v1/shared_types.go
index 800291b..64425e7 100644
--- a/api/v1/shared_types.go
+++ b/api/v1/shared_types.go
@@ -32,6 +32,7 @@ package v1
 //
 // * "example.com/bar" - "/" is an invalid character
 //
+// +kubebuilder:validation:MinLength=0
 // +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])?)*$`
 type Group string
@@ -127,3 +128,15 @@ type LabelKey string
 // +kubebuilder:validation:MaxLength=63
 // +kubebuilder:validation:Pattern=`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$`
 type LabelValue string
+
+// LabelSelector defines a query for resources based on their labels.
+// This simplified version uses only the matchLabels field.
+type LabelSelector struct {
+	// 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.
+	//
+	// +required
+	// +kubebuilder:validation:MaxItems=64
+	MatchLabels map[LabelKey]LabelValue `json:"matchLabels,omitempty" protobuf:"bytes,1,rep,name=matchLabels"`
+}
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index adf19ac..0cdf170 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -25,89 +25,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *EndpointPickerConfig) DeepCopyInto(out *EndpointPickerConfig) {
-	*out = *in
-	if in.ExtensionRef != nil {
-		in, out := &in.ExtensionRef, &out.ExtensionRef
-		*out = new(Extension)
-		(*in).DeepCopyInto(*out)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointPickerConfig.
-func (in *EndpointPickerConfig) DeepCopy() *EndpointPickerConfig {
-	if in == nil {
-		return nil
-	}
-	out := new(EndpointPickerConfig)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Extension) DeepCopyInto(out *Extension) {
-	*out = *in
-	in.ExtensionReference.DeepCopyInto(&out.ExtensionReference)
-	in.ExtensionConnection.DeepCopyInto(&out.ExtensionConnection)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension.
-func (in *Extension) DeepCopy() *Extension {
-	if in == nil {
-		return nil
-	}
-	out := new(Extension)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExtensionConnection) DeepCopyInto(out *ExtensionConnection) {
-	*out = *in
-	if in.FailureMode != nil {
-		in, out := &in.FailureMode, &out.FailureMode
-		*out = new(ExtensionFailureMode)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConnection.
-func (in *ExtensionConnection) DeepCopy() *ExtensionConnection {
-	if in == nil {
-		return nil
-	}
-	out := new(ExtensionConnection)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExtensionReference) DeepCopyInto(out *ExtensionReference) {
 	*out = *in
 	if in.Group != nil {
 		in, out := &in.Group, &out.Group
 		*out = new(Group)
 		**out = **in
 	}
-	if in.Kind != nil {
-		in, out := &in.Kind, &out.Kind
-		*out = new(Kind)
-		**out = **in
-	}
-	if in.PortNumber != nil {
-		in, out := &in.PortNumber, &out.PortNumber
-		*out = new(PortNumber)
-		**out = **in
-	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionReference.
-func (in *ExtensionReference) DeepCopy() *ExtensionReference {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension.
+func (in *Extension) DeepCopy() *Extension {
 	if in == nil {
 		return nil
 	}
-	out := new(ExtensionReference)
+	out := new(Extension)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -174,14 +107,13 @@ func (in *InferencePoolList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *InferencePoolSpec) DeepCopyInto(out *InferencePoolSpec) {
 	*out = *in
-	if in.Selector != nil {
-		in, out := &in.Selector, &out.Selector
-		*out = make(map[LabelKey]LabelValue, len(*in))
-		for key, val := range *in {
-			(*out)[key] = val
-		}
+	in.Selector.DeepCopyInto(&out.Selector)
+	if in.TargetPorts != nil {
+		in, out := &in.TargetPorts, &out.TargetPorts
+		*out = make([]Port, len(*in))
+		copy(*out, *in)
 	}
-	in.EndpointPickerConfig.DeepCopyInto(&out.EndpointPickerConfig)
+	in.ExtensionRef.DeepCopyInto(&out.ExtensionRef)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferencePoolSpec.
@@ -216,6 +148,28 @@ func (in *InferencePoolStatus) DeepCopy() *InferencePoolStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LabelSelector) DeepCopyInto(out *LabelSelector) {
+	*out = *in
+	if in.MatchLabels != nil {
+		in, out := &in.MatchLabels, &out.MatchLabels
+		*out = make(map[LabelKey]LabelValue, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelSelector.
+func (in *LabelSelector) DeepCopy() *LabelSelector {
+	if in == nil {
+		return nil
+	}
+	out := new(LabelSelector)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ParentGatewayReference) DeepCopyInto(out *ParentGatewayReference) {
 	*out = *in
@@ -224,16 +178,6 @@ func (in *ParentGatewayReference) DeepCopyInto(out *ParentGatewayReference) {
 		*out = new(Group)
 		**out = **in
 	}
-	if in.Kind != nil {
-		in, out := &in.Kind, &out.Kind
-		*out = new(Kind)
-		**out = **in
-	}
-	if in.Namespace != nil {
-		in, out := &in.Namespace, &out.Namespace
-		*out = new(Namespace)
-		**out = **in
-	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParentGatewayReference.
@@ -249,7 +193,6 @@ func (in *ParentGatewayReference) DeepCopy() *ParentGatewayReference {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PoolStatus) DeepCopyInto(out *PoolStatus) {
 	*out = *in
-	in.GatewayRef.DeepCopyInto(&out.GatewayRef)
 	if in.Conditions != nil {
 		in, out := &in.Conditions, &out.Conditions
 		*out = make([]metav1.Condition, len(*in))
@@ -257,6 +200,7 @@ func (in *PoolStatus) DeepCopyInto(out *PoolStatus) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	in.GatewayRef.DeepCopyInto(&out.GatewayRef)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PoolStatus.
@@ -268,3 +212,18 @@ func (in *PoolStatus) DeepCopy() *PoolStatus {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Port) DeepCopyInto(out *Port) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Port.
+func (in *Port) DeepCopy() *Port {
+	if in == nil {
+		return nil
+	}
+	out := new(Port)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/apix/v1alpha2/inferenceobjective_types.go b/apix/v1alpha2/inferenceobjective_types.go
index 4af3e27..07f2c4d 100644
--- a/apix/v1alpha2/inferenceobjective_types.go
+++ b/apix/v1alpha2/inferenceobjective_types.go
@@ -27,7 +27,7 @@ import (
 // +kubebuilder:storageversion
 // +kubebuilder:printcolumn:name="Model Name",type=string,JSONPath=`.spec.modelName`
 // +kubebuilder:printcolumn:name="Inference Pool",type=string,JSONPath=`.spec.poolRef.name`
-// +kubebuilder:printcolumn:name="Criticality",type=string,JSONPath=`.spec.criticality`
+// +kubebuilder:printcolumn:name="Priority",type=string,JSONPath=`.spec.priority`
 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
 // +genclient
 type InferenceObjective struct {
@@ -64,25 +64,20 @@ type InferenceObjectiveList struct {
 // condition, one will be selected at random.
 type InferenceObjectiveSpec struct {
 
-	// Criticality defines how important it is to serve the model compared to other models referencing the same pool.
-	// Criticality impacts how traffic is handled in resource constrained situations. It handles this by
-	// queuing or rejecting requests of lower criticality. InferenceObjectives of an equivalent Criticality will
-	// fairly share resources over throughput of tokens. In the future, the metric used to calculate fairness,
-	// and the proportionality of fairness will be configurable.
-	//
-	// Default values for this field will not be set, to allow for future additions of new field that may 'one of' with this field.
-	// Any implementations that may consume this field may treat an unset value as the 'Standard' range.
+	// Priority defines how important it is to serve the request compared to other requests in the same pool.
+	// Priority is an integer value that defines the priority of the request.
+	// The higher the value, the more critical the request is; negative values _are_ allowed.
+	// No default value is set for this field, allowing for future additions of new fields that may 'one of' with this field.
+	// However, implementations that consume this field (such as the Endpoint Picker) will treat an unset value as '0'.
+	// Priority is used in flow control, primarily in the event of resource scarcity(reqeusts need to be queued).
+	// All requests will be queued, and flow control will _always_ allow requests of higher priority to be served first.
+	// Fairness is only enforced and tracked between requests of the same priority.
+	//
+	// Example: requests with Priority 10 will always be served before
+	// requests with Priority of 0 (the value used if Priority is unset or no InfereneceObjective is specified).
+	// Similarly requests with a Priority of -10 will always be served after requests with Priority of 0.
 	// +optional
-	Criticality *Criticality `json:"criticality,omitempty"`
-
-	// TargetModels allow multiple versions of a model for traffic splitting.
-	// If not specified, the target model name is defaulted to the modelName parameter.
-	// modelName is often in reference to a LoRA adapter.
-	//
-	// +optional
-	// +kubebuilder:validation:MaxItems=10
-	// +kubebuilder:validation:XValidation:message="Weights should be set for all models, or none of the models.",rule="self.all(model, has(model.weight)) || self.all(model, !has(model.weight))"
-	TargetModels []TargetModel `json:"targetModels,omitempty"`
+	Priority *int `json:"priority,omitempty"`
 
 	// PoolRef is a reference to the inference pool, the pool must exist in the same namespace.
 	//
@@ -96,7 +91,7 @@ type PoolObjectReference struct {
 	// Group is the group of the referent.
 	//
 	// +optional
-	// +kubebuilder:default="inference.networking.x-k8s.io"
+	// +kubebuilder:default="inference.networking.k8s.io"
 	Group Group `json:"group,omitempty"`
 
 	// Kind is kind of the referent. For example "InferencePool".
@@ -111,59 +106,6 @@ type PoolObjectReference struct {
 	Name ObjectName `json:"name"`
 }
 
-// Criticality defines how important it is to serve the model compared to other models.
-// Criticality is intentionally a bounded enum to contain the possibilities that need to be supported by the load balancing algorithm. Any reference to the Criticality field must be optional (use a pointer), and set no default.
-// This allows us to union this with a oneOf field in the future should we wish to adjust/extend this behavior.
-// +kubebuilder:validation:Enum=Critical;Standard;Sheddable
-type Criticality string
-
-const (
-	// Critical defines the highest level of criticality. Requests to this band will be shed last.
-	Critical Criticality = "Critical"
-
-	// Standard defines the base criticality level and is more important than Sheddable but less
-	// important than Critical. Requests in this band will be shed before critical traffic.
-	// Most models are expected to fall within this band.
-	Standard Criticality = "Standard"
-
-	// Sheddable defines the lowest level of criticality. Requests to this band will be shed before
-	// all other bands.
-	Sheddable Criticality = "Sheddable"
-)
-
-// TargetModel represents a deployed model or a LoRA adapter. The
-// Name field is expected to match the name of the LoRA adapter
-// (or base model) as it is registered within the model server. Inference
-// Gateway assumes that the model exists on the model server and it's the
-// responsibility of the user to validate a correct match. Should a model fail
-// to exist at request time, the error is processed by the Inference Gateway
-// and emitted on the appropriate InferenceObjective object.
-type TargetModel struct {
-	// Name is the name of the adapter or base model, as expected by the ModelServer.
-	//
-	// +kubebuilder:validation:MaxLength=253
-	// +kubebuilder:validation:Required
-	Name string `json:"name"`
-
-	// Weight is used to determine the proportion of traffic that should be
-	// sent to this model when multiple target models are specified.
-	//
-	// Weight defines the proportion of requests forwarded to the specified
-	// model. This is computed as weight/(sum of all weights in this
-	// TargetModels list). For non-zero values, there may be some epsilon from
-	// the exact proportion defined here depending on the precision an
-	// implementation supports. Weight is not a percentage and the sum of
-	// weights does not need to equal 100.
-	//
-	// If a weight is set for any targetModel, it must be set for all targetModels.
-	// Conversely weights are optional, so long as ALL targetModels do not specify a weight.
-	//
-	// +optional
-	// +kubebuilder:validation:Minimum=1
-	// +kubebuilder:validation:Maximum=1000000
-	Weight *int32 `json:"weight,omitempty"`
-}
-
 // InferenceObjectiveStatus defines the observed state of InferenceObjective
 type InferenceObjectiveStatus struct {
 	// Conditions track the state of the InferenceObjective.
diff --git a/apix/v1alpha2/inferencepool_conversion.go b/apix/v1alpha2/inferencepool_conversion.go
new file mode 100644
index 0000000..aa29d8f
--- /dev/null
+++ b/apix/v1alpha2/inferencepool_conversion.go
@@ -0,0 +1,149 @@
+/*
+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 v1alpha2
+
+import (
+	"errors"
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+
+	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+// ConvertTo converts this InferencePool (v1alpha2) to the v1 version.
+func (src *InferencePool) ConvertTo(dst *v1.InferencePool) error {
+	if dst == nil {
+		return errors.New("dst cannot be nil")
+	}
+	v1Extension, err := convertExtensionRefToV1(&src.Spec.ExtensionRef)
+	if err != nil {
+		return err
+	}
+	v1Status, err := convertStatusToV1(&src.Status)
+	if err != nil {
+		return err
+	}
+	dst.TypeMeta = src.TypeMeta
+	dst.ObjectMeta = src.ObjectMeta
+	dst.Spec.TargetPorts = []v1.Port{{Number: v1.PortNumber(src.Spec.TargetPortNumber)}}
+	dst.Spec.ExtensionRef = v1Extension
+	dst.Status = *v1Status
+	if src.Spec.Selector != nil {
+		dst.Spec.Selector.MatchLabels = make(map[v1.LabelKey]v1.LabelValue, len(src.Spec.Selector))
+		for k, v := range src.Spec.Selector {
+			dst.Spec.Selector.MatchLabels[v1.LabelKey(k)] = v1.LabelValue(v)
+		}
+	}
+	return nil
+}
+
+// ConvertFrom converts from the v1 version to this version (v1alpha2).
+func (dst *InferencePool) ConvertFrom(src *v1.InferencePool) error {
+	if src == nil {
+		return errors.New("src cannot be nil")
+	}
+	extensionRef, err := convertExtensionRefFromV1(&src.Spec.ExtensionRef)
+	if err != nil {
+		return err
+	}
+	status, err := convertStatusFromV1(&src.Status)
+	if err != nil {
+		return err
+	}
+	dst.TypeMeta = src.TypeMeta
+	dst.ObjectMeta = src.ObjectMeta
+	dst.Spec.TargetPortNumber = int32(src.Spec.TargetPorts[0].Number)
+	dst.Spec.ExtensionRef = extensionRef
+	dst.Status = *status
+	if src.Spec.Selector.MatchLabels != nil {
+		dst.Spec.Selector = make(map[LabelKey]LabelValue, len(src.Spec.Selector.MatchLabels))
+		for k, v := range src.Spec.Selector.MatchLabels {
+			dst.Spec.Selector[LabelKey(k)] = LabelValue(v)
+		}
+	}
+	return nil
+}
+
+func convertStatusToV1(src *InferencePoolStatus) (*v1.InferencePoolStatus, error) {
+	if src == nil {
+		return nil, errors.New("src cannot be nil")
+	}
+	u, err := toUnstructured(src)
+	if err != nil {
+		return nil, err
+	}
+	return convert[v1.InferencePoolStatus](u)
+}
+
+func convertStatusFromV1(src *v1.InferencePoolStatus) (*InferencePoolStatus, error) {
+	if src == nil {
+		return nil, errors.New("src cannot be nil")
+	}
+	u, err := toUnstructured(src)
+	if err != nil {
+		return nil, err
+	}
+	return convert[InferencePoolStatus](u)
+}
+
+func convertExtensionRefToV1(src *Extension) (v1.Extension, error) {
+	if src == nil {
+		return v1.Extension{}, errors.New("src cannot be nil")
+	}
+	u, err := toUnstructured(src)
+	if err != nil {
+		return v1.Extension{}, err
+	}
+	out, err := convert[v1.Extension](u)
+	if err != nil {
+		return v1.Extension{}, err
+	}
+	return *out, nil
+}
+
+func convertExtensionRefFromV1(src *v1.Extension) (Extension, error) {
+	if src == nil {
+		return Extension{}, errors.New("src cannot be nil")
+	}
+	u, err := toUnstructured(&src)
+	if err != nil {
+		return Extension{}, err
+	}
+	extension, err := convert[Extension](u)
+	if err != nil {
+		return Extension{}, err
+	}
+	return *extension, nil
+}
+
+func toUnstructured(obj any) (*unstructured.Unstructured, error) {
+	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+	if err != nil {
+		return nil, err
+	}
+	return &unstructured.Unstructured{Object: u}, nil
+}
+
+func convert[T any](u *unstructured.Unstructured) (*T, error) {
+	var res T
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &res); err != nil {
+		return nil, fmt.Errorf("error converting unstructured to T: %v", err)
+	}
+	return &res, nil
+}
diff --git a/apix/v1alpha2/inferencepool_types.go b/apix/v1alpha2/inferencepool_types.go
index 0213d86..7ccf215 100644
--- a/apix/v1alpha2/inferencepool_types.go
+++ b/apix/v1alpha2/inferencepool_types.go
@@ -21,10 +21,9 @@ import (
 )
 
 // InferencePool is the Schema for the InferencePools API.
-//
 // +kubebuilder:object:root=true
-// TODO: change the annotation once it gets officially approved
 // +kubebuilder:metadata:annotations="api-approved.kubernetes.io=unapproved, experimental-only"
+// +kubebuilder:resource:shortName=xinfpool
 // +kubebuilder:subresource:status
 // +kubebuilder:storageversion
 // +genclient
@@ -69,42 +68,13 @@ type InferencePoolSpec struct {
 	// +kubebuilder:validation:Required
 	TargetPortNumber int32 `json:"targetPortNumber"`
 
-	// EndpointPickerConfig specifies the configuration needed by the proxy to discover and connect to the endpoint
-	// picker service that picks endpoints for the requests routed to this pool.
-	EndpointPickerConfig `json:",inline"`
-}
-
-// EndpointPickerConfig specifies the configuration needed by the proxy to discover and connect to the endpoint picker extension.
-// This type is intended to be a union of mutually exclusive configuration options that we may add in the future.
-type EndpointPickerConfig struct {
 	// Extension configures an endpoint picker as an extension service.
-	//
-	// +kubebuilder:validation:Required
-	ExtensionRef *Extension `json:"extensionRef,omitempty"`
+	// +required
+	ExtensionRef Extension `json:"extensionRef,omitempty"`
 }
 
 // Extension specifies how to configure an extension that runs the endpoint picker.
 type Extension struct {
-	// Reference is a reference to a service extension. When ExtensionReference is invalid,
-	// a 5XX status code MUST be returned for the request that would have otherwise been routed
-	// to the invalid backend.
-	ExtensionReference `json:",inline"`
-
-	// ExtensionConnection configures the connection between the Gateway and the extension.
-	ExtensionConnection `json:",inline"`
-}
-
-// ExtensionReference is a reference to the extension.
-//
-// Connections to this extension MUST use TLS by default. Implementations MAY
-// provide a way to customize this connection to use cleartext, a different
-// protocol, or custom TLS configuration.
-//
-// If a reference is invalid, the implementation MUST update the `ResolvedRefs`
-// Condition on the InferencePool's status to `status: False`. A 5XX status code
-// MUST be returned for the request that would have otherwise been routed to the
-// invalid backend.
-type ExtensionReference struct {
 	// Group is the group of the referent.
 	// The default value is "", representing the Core API group.
 	//
@@ -137,10 +107,7 @@ type ExtensionReference struct {
 	//
 	// +optional
 	PortNumber *PortNumber `json:"portNumber,omitempty"`
-}
 
-// ExtensionConnection encapsulates options that configures the connection to the extension.
-type ExtensionConnection struct {
 	// Configures how the gateway handles the case when the extension is not responsive.
 	// Defaults to failClose.
 	//
@@ -266,7 +233,7 @@ const (
 	InferencePoolReasonResolvedRefs InferencePoolReason = "ResolvedRefs"
 
 	// This reason is used with the "ResolvedRefs" condition when the
-	// ExtensionRef is invalid in some way. This can include an unsupported kind
+	// Extension is invalid in some way. This can include an unsupported kind
 	// or API group, or a reference to a resource that can not be found.
 	InferencePoolReasonInvalidExtensionRef InferencePoolReason = "InvalidExtensionRef"
 )
diff --git a/apix/v1alpha2/zz_generated.deepcopy.go b/apix/v1alpha2/zz_generated.deepcopy.go
index 697c443..5a71e75 100644
--- a/apix/v1alpha2/zz_generated.deepcopy.go
+++ b/apix/v1alpha2/zz_generated.deepcopy.go
@@ -22,68 +22,11 @@ package v1alpha2
 
 import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1"
-	runtime "k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime"
 )
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *EndpointPickerConfig) DeepCopyInto(out *EndpointPickerConfig) {
-	*out = *in
-	if in.ExtensionRef != nil {
-		in, out := &in.ExtensionRef, &out.ExtensionRef
-		*out = new(Extension)
-		(*in).DeepCopyInto(*out)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointPickerConfig.
-func (in *EndpointPickerConfig) DeepCopy() *EndpointPickerConfig {
-	if in == nil {
-		return nil
-	}
-	out := new(EndpointPickerConfig)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Extension) DeepCopyInto(out *Extension) {
-	*out = *in
-	in.ExtensionReference.DeepCopyInto(&out.ExtensionReference)
-	in.ExtensionConnection.DeepCopyInto(&out.ExtensionConnection)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension.
-func (in *Extension) DeepCopy() *Extension {
-	if in == nil {
-		return nil
-	}
-	out := new(Extension)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExtensionConnection) DeepCopyInto(out *ExtensionConnection) {
-	*out = *in
-	if in.FailureMode != nil {
-		in, out := &in.FailureMode, &out.FailureMode
-		*out = new(ExtensionFailureMode)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionConnection.
-func (in *ExtensionConnection) DeepCopy() *ExtensionConnection {
-	if in == nil {
-		return nil
-	}
-	out := new(ExtensionConnection)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExtensionReference) DeepCopyInto(out *ExtensionReference) {
 	*out = *in
 	if in.Group != nil {
 		in, out := &in.Group, &out.Group
@@ -100,14 +43,19 @@ func (in *ExtensionReference) DeepCopyInto(out *ExtensionReference) {
 		*out = new(PortNumber)
 		**out = **in
 	}
+	if in.FailureMode != nil {
+		in, out := &in.FailureMode, &out.FailureMode
+		*out = new(ExtensionFailureMode)
+		**out = **in
+	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionReference.
-func (in *ExtensionReference) DeepCopy() *ExtensionReference {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension.
+func (in *Extension) DeepCopy() *Extension {
 	if in == nil {
 		return nil
 	}
-	out := new(ExtensionReference)
+	out := new(Extension)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -174,18 +122,11 @@ func (in *InferenceObjectiveList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *InferenceObjectiveSpec) DeepCopyInto(out *InferenceObjectiveSpec) {
 	*out = *in
-	if in.Criticality != nil {
-		in, out := &in.Criticality, &out.Criticality
-		*out = new(Criticality)
+	if in.Priority != nil {
+		in, out := &in.Priority, &out.Priority
+		*out = new(int)
 		**out = **in
 	}
-	if in.TargetModels != nil {
-		in, out := &in.TargetModels, &out.TargetModels
-		*out = make([]TargetModel, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
 	out.PoolRef = in.PoolRef
 }
 
@@ -290,7 +231,7 @@ func (in *InferencePoolSpec) DeepCopyInto(out *InferencePoolSpec) {
 			(*out)[key] = val
 		}
 	}
-	in.EndpointPickerConfig.DeepCopyInto(&out.EndpointPickerConfig)
+	in.ExtensionRef.DeepCopyInto(&out.ExtensionRef)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferencePoolSpec.
@@ -392,23 +333,3 @@ func (in *PoolStatus) DeepCopy() *PoolStatus {
 	in.DeepCopyInto(out)
 	return out
 }
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *TargetModel) DeepCopyInto(out *TargetModel) {
-	*out = *in
-	if in.Weight != nil {
-		in, out := &in.Weight, &out.Weight
-		*out = new(int32)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetModel.
-func (in *TargetModel) DeepCopy() *TargetModel {
-	if in == nil {
-		return nil
-	}
-	out := new(TargetModel)
-	in.DeepCopyInto(out)
-	return out
-}
diff --git a/client-go/applyconfiguration/api/v1/endpointpickerconfig.go b/client-go/applyconfiguration/api/v1/endpointpickerconfig.go
deleted file mode 100644
index 9877d40..0000000
--- a/client-go/applyconfiguration/api/v1/endpointpickerconfig.go
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-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 v1
-
-// EndpointPickerConfigApplyConfiguration represents a declarative configuration of the EndpointPickerConfig type for use
-// with apply.
-type EndpointPickerConfigApplyConfiguration struct {
-	ExtensionRef *ExtensionApplyConfiguration `json:"extensionRef,omitempty"`
-}
-
-// EndpointPickerConfigApplyConfiguration constructs a declarative configuration of the EndpointPickerConfig type for use with
-// apply.
-func EndpointPickerConfig() *EndpointPickerConfigApplyConfiguration {
-	return &EndpointPickerConfigApplyConfiguration{}
-}
-
-// WithExtensionRef sets the ExtensionRef 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 ExtensionRef field is set to the value of the last call.
-func (b *EndpointPickerConfigApplyConfiguration) WithExtensionRef(value *ExtensionApplyConfiguration) *EndpointPickerConfigApplyConfiguration {
-	b.ExtensionRef = value
-	return b
-}
diff --git a/client-go/applyconfiguration/api/v1/extension.go b/client-go/applyconfiguration/api/v1/extension.go
index b14ec83..7e7d645 100644
--- a/client-go/applyconfiguration/api/v1/extension.go
+++ b/client-go/applyconfiguration/api/v1/extension.go
@@ -25,8 +25,11 @@ import (
 // ExtensionApplyConfiguration represents a declarative configuration of the Extension type for use
 // with apply.
 type ExtensionApplyConfiguration struct {
-	ExtensionReferenceApplyConfiguration  `json:",inline"`
-	ExtensionConnectionApplyConfiguration `json:",inline"`
+	Group       *apiv1.Group                `json:"group,omitempty"`
+	Kind        *apiv1.Kind                 `json:"kind,omitempty"`
+	Name        *apiv1.ObjectName           `json:"name,omitempty"`
+	PortNumber  *apiv1.PortNumber           `json:"portNumber,omitempty"`
+	FailureMode *apiv1.ExtensionFailureMode `json:"failureMode,omitempty"`
 }
 
 // ExtensionApplyConfiguration constructs a declarative configuration of the Extension type for use with
@@ -39,7 +42,7 @@ func Extension() *ExtensionApplyConfiguration {
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the Group field is set to the value of the last call.
 func (b *ExtensionApplyConfiguration) WithGroup(value apiv1.Group) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.Group = &value
+	b.Group = &value
 	return b
 }
 
@@ -47,7 +50,7 @@ func (b *ExtensionApplyConfiguration) WithGroup(value apiv1.Group) *ExtensionApp
 // 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 *ExtensionApplyConfiguration) WithKind(value apiv1.Kind) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.Kind = &value
+	b.Kind = &value
 	return b
 }
 
@@ -55,7 +58,7 @@ func (b *ExtensionApplyConfiguration) WithKind(value apiv1.Kind) *ExtensionApply
 // 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 *ExtensionApplyConfiguration) WithName(value apiv1.ObjectName) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.Name = &value
+	b.Name = &value
 	return b
 }
 
@@ -63,7 +66,7 @@ func (b *ExtensionApplyConfiguration) WithName(value apiv1.ObjectName) *Extensio
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the PortNumber field is set to the value of the last call.
 func (b *ExtensionApplyConfiguration) WithPortNumber(value apiv1.PortNumber) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.PortNumber = &value
+	b.PortNumber = &value
 	return b
 }
 
@@ -71,6 +74,6 @@ func (b *ExtensionApplyConfiguration) WithPortNumber(value apiv1.PortNumber) *Ex
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the FailureMode field is set to the value of the last call.
 func (b *ExtensionApplyConfiguration) WithFailureMode(value apiv1.ExtensionFailureMode) *ExtensionApplyConfiguration {
-	b.ExtensionConnectionApplyConfiguration.FailureMode = &value
+	b.FailureMode = &value
 	return b
 }
diff --git a/client-go/applyconfiguration/api/v1/extensionreference.go b/client-go/applyconfiguration/api/v1/extensionreference.go
deleted file mode 100644
index 21402d1..0000000
--- a/client-go/applyconfiguration/api/v1/extensionreference.go
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-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 v1
-
-import (
-	apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
-)
-
-// ExtensionReferenceApplyConfiguration represents a declarative configuration of the ExtensionReference type for use
-// with apply.
-type ExtensionReferenceApplyConfiguration struct {
-	Group      *apiv1.Group      `json:"group,omitempty"`
-	Kind       *apiv1.Kind       `json:"kind,omitempty"`
-	Name       *apiv1.ObjectName `json:"name,omitempty"`
-	PortNumber *apiv1.PortNumber `json:"portNumber,omitempty"`
-}
-
-// ExtensionReferenceApplyConfiguration constructs a declarative configuration of the ExtensionReference type for use with
-// apply.
-func ExtensionReference() *ExtensionReferenceApplyConfiguration {
-	return &ExtensionReferenceApplyConfiguration{}
-}
-
-// WithGroup sets the Group 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 Group field is set to the value of the last call.
-func (b *ExtensionReferenceApplyConfiguration) WithGroup(value apiv1.Group) *ExtensionReferenceApplyConfiguration {
-	b.Group = &value
-	return b
-}
-
-// 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 *ExtensionReferenceApplyConfiguration) WithKind(value apiv1.Kind) *ExtensionReferenceApplyConfiguration {
-	b.Kind = &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 *ExtensionReferenceApplyConfiguration) WithName(value apiv1.ObjectName) *ExtensionReferenceApplyConfiguration {
-	b.Name = &value
-	return b
-}
-
-// WithPortNumber sets the PortNumber 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 PortNumber field is set to the value of the last call.
-func (b *ExtensionReferenceApplyConfiguration) WithPortNumber(value apiv1.PortNumber) *ExtensionReferenceApplyConfiguration {
-	b.PortNumber = &value
-	return b
-}
diff --git a/client-go/applyconfiguration/api/v1/inferencepoolspec.go b/client-go/applyconfiguration/api/v1/inferencepoolspec.go
index d9c08b2..364d059 100644
--- a/client-go/applyconfiguration/api/v1/inferencepoolspec.go
+++ b/client-go/applyconfiguration/api/v1/inferencepoolspec.go
@@ -18,16 +18,12 @@ limitations under the License.
 
 package v1
 
-import (
-	apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
-)
-
 // InferencePoolSpecApplyConfiguration represents a declarative configuration of the InferencePoolSpec type for use
 // with apply.
 type InferencePoolSpecApplyConfiguration struct {
-	Selector                               map[apiv1.LabelKey]apiv1.LabelValue `json:"selector,omitempty"`
-	TargetPortNumber                       *int32                              `json:"targetPortNumber,omitempty"`
-	EndpointPickerConfigApplyConfiguration `json:",inline"`
+	Selector     *LabelSelectorApplyConfiguration `json:"selector,omitempty"`
+	TargetPorts  []PortApplyConfiguration         `json:"targetPorts,omitempty"`
+	ExtensionRef *ExtensionApplyConfiguration     `json:"extensionRef,omitempty"`
 }
 
 // InferencePoolSpecApplyConfiguration constructs a declarative configuration of the InferencePoolSpec type for use with
@@ -36,25 +32,24 @@ func InferencePoolSpec() *InferencePoolSpecApplyConfiguration {
 	return &InferencePoolSpecApplyConfiguration{}
 }
 
-// WithSelector puts the entries into the Selector 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 Selector field,
-// overwriting an existing map entries in Selector field with the same key.
-func (b *InferencePoolSpecApplyConfiguration) WithSelector(entries map[apiv1.LabelKey]apiv1.LabelValue) *InferencePoolSpecApplyConfiguration {
-	if b.Selector == nil && len(entries) > 0 {
-		b.Selector = make(map[apiv1.LabelKey]apiv1.LabelValue, len(entries))
-	}
-	for k, v := range entries {
-		b.Selector[k] = v
-	}
+// WithSelector sets the Selector 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 Selector field is set to the value of the last call.
+func (b *InferencePoolSpecApplyConfiguration) WithSelector(value *LabelSelectorApplyConfiguration) *InferencePoolSpecApplyConfiguration {
+	b.Selector = value
 	return b
 }
 
-// WithTargetPortNumber sets the TargetPortNumber 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 TargetPortNumber field is set to the value of the last call.
-func (b *InferencePoolSpecApplyConfiguration) WithTargetPortNumber(value int32) *InferencePoolSpecApplyConfiguration {
-	b.TargetPortNumber = &value
+// WithTargetPorts adds the given value to the TargetPorts 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 TargetPorts field.
+func (b *InferencePoolSpecApplyConfiguration) WithTargetPorts(values ...*PortApplyConfiguration) *InferencePoolSpecApplyConfiguration {
+	for i := range values {
+		if values[i] == nil {
+			panic("nil value passed to WithTargetPorts")
+		}
+		b.TargetPorts = append(b.TargetPorts, *values[i])
+	}
 	return b
 }
 
@@ -62,6 +57,6 @@ func (b *InferencePoolSpecApplyConfiguration) WithTargetPortNumber(value int32)
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the ExtensionRef field is set to the value of the last call.
 func (b *InferencePoolSpecApplyConfiguration) WithExtensionRef(value *ExtensionApplyConfiguration) *InferencePoolSpecApplyConfiguration {
-	b.EndpointPickerConfigApplyConfiguration.ExtensionRef = value
+	b.ExtensionRef = value
 	return b
 }
diff --git a/client-go/applyconfiguration/api/v1/labelselector.go b/client-go/applyconfiguration/api/v1/labelselector.go
new file mode 100644
index 0000000..70953db
--- /dev/null
+++ b/client-go/applyconfiguration/api/v1/labelselector.go
@@ -0,0 +1,49 @@
+/*
+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 v1
+
+import (
+	apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+// LabelSelectorApplyConfiguration represents a declarative configuration of the LabelSelector type for use
+// with apply.
+type LabelSelectorApplyConfiguration struct {
+	MatchLabels map[apiv1.LabelKey]apiv1.LabelValue `json:"matchLabels,omitempty"`
+}
+
+// LabelSelectorApplyConfiguration constructs a declarative configuration of the LabelSelector type for use with
+// apply.
+func LabelSelector() *LabelSelectorApplyConfiguration {
+	return &LabelSelectorApplyConfiguration{}
+}
+
+// WithMatchLabels puts the entries into the MatchLabels 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 MatchLabels field,
+// overwriting an existing map entries in MatchLabels field with the same key.
+func (b *LabelSelectorApplyConfiguration) WithMatchLabels(entries map[apiv1.LabelKey]apiv1.LabelValue) *LabelSelectorApplyConfiguration {
+	if b.MatchLabels == nil && len(entries) > 0 {
+		b.MatchLabels = make(map[apiv1.LabelKey]apiv1.LabelValue, len(entries))
+	}
+	for k, v := range entries {
+		b.MatchLabels[k] = v
+	}
+	return b
+}
diff --git a/client-go/applyconfiguration/api/v1/poolstatus.go b/client-go/applyconfiguration/api/v1/poolstatus.go
index 64fe2dc..6c582d8 100644
--- a/client-go/applyconfiguration/api/v1/poolstatus.go
+++ b/client-go/applyconfiguration/api/v1/poolstatus.go
@@ -25,8 +25,8 @@ import (
 // PoolStatusApplyConfiguration represents a declarative configuration of the PoolStatus type for use
 // with apply.
 type PoolStatusApplyConfiguration struct {
-	GatewayRef *ParentGatewayReferenceApplyConfiguration `json:"parentRef,omitempty"`
 	Conditions []metav1.ConditionApplyConfiguration      `json:"conditions,omitempty"`
+	GatewayRef *ParentGatewayReferenceApplyConfiguration `json:"parentRef,omitempty"`
 }
 
 // PoolStatusApplyConfiguration constructs a declarative configuration of the PoolStatus type for use with
@@ -35,14 +35,6 @@ func PoolStatus() *PoolStatusApplyConfiguration {
 	return &PoolStatusApplyConfiguration{}
 }
 
-// WithGatewayRef sets the GatewayRef 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 GatewayRef field is set to the value of the last call.
-func (b *PoolStatusApplyConfiguration) WithGatewayRef(value *ParentGatewayReferenceApplyConfiguration) *PoolStatusApplyConfiguration {
-	b.GatewayRef = value
-	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.
@@ -55,3 +47,11 @@ func (b *PoolStatusApplyConfiguration) WithConditions(values ...*metav1.Conditio
 	}
 	return b
 }
+
+// WithGatewayRef sets the GatewayRef 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 GatewayRef field is set to the value of the last call.
+func (b *PoolStatusApplyConfiguration) WithGatewayRef(value *ParentGatewayReferenceApplyConfiguration) *PoolStatusApplyConfiguration {
+	b.GatewayRef = value
+	return b
+}
diff --git a/client-go/applyconfiguration/api/v1/extensionconnection.go b/client-go/applyconfiguration/api/v1/port.go
similarity index 51%
rename from client-go/applyconfiguration/api/v1/extensionconnection.go
rename to client-go/applyconfiguration/api/v1/port.go
index cc07827..6067a5d 100644
--- a/client-go/applyconfiguration/api/v1/extensionconnection.go
+++ b/client-go/applyconfiguration/api/v1/port.go
@@ -22,22 +22,22 @@ import (
 	apiv1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 )
 
-// ExtensionConnectionApplyConfiguration represents a declarative configuration of the ExtensionConnection type for use
+// PortApplyConfiguration represents a declarative configuration of the Port type for use
 // with apply.
-type ExtensionConnectionApplyConfiguration struct {
-	FailureMode *apiv1.ExtensionFailureMode `json:"failureMode,omitempty"`
+type PortApplyConfiguration struct {
+	Number *apiv1.PortNumber `json:"number,omitempty"`
 }
 
-// ExtensionConnectionApplyConfiguration constructs a declarative configuration of the ExtensionConnection type for use with
+// PortApplyConfiguration constructs a declarative configuration of the Port type for use with
 // apply.
-func ExtensionConnection() *ExtensionConnectionApplyConfiguration {
-	return &ExtensionConnectionApplyConfiguration{}
+func Port() *PortApplyConfiguration {
+	return &PortApplyConfiguration{}
 }
 
-// WithFailureMode sets the FailureMode field in the declarative configuration to the given value
+// WithNumber sets the Number 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 FailureMode field is set to the value of the last call.
-func (b *ExtensionConnectionApplyConfiguration) WithFailureMode(value apiv1.ExtensionFailureMode) *ExtensionConnectionApplyConfiguration {
-	b.FailureMode = &value
+// If called multiple times, the Number field is set to the value of the last call.
+func (b *PortApplyConfiguration) WithNumber(value apiv1.PortNumber) *PortApplyConfiguration {
+	b.Number = &value
 	return b
 }
diff --git a/client-go/applyconfiguration/apix/v1alpha2/endpointpickerconfig.go b/client-go/applyconfiguration/apix/v1alpha2/endpointpickerconfig.go
deleted file mode 100644
index 39d4d8d..0000000
--- a/client-go/applyconfiguration/apix/v1alpha2/endpointpickerconfig.go
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-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 v1alpha2
-
-// EndpointPickerConfigApplyConfiguration represents a declarative configuration of the EndpointPickerConfig type for use
-// with apply.
-type EndpointPickerConfigApplyConfiguration struct {
-	ExtensionRef *ExtensionApplyConfiguration `json:"extensionRef,omitempty"`
-}
-
-// EndpointPickerConfigApplyConfiguration constructs a declarative configuration of the EndpointPickerConfig type for use with
-// apply.
-func EndpointPickerConfig() *EndpointPickerConfigApplyConfiguration {
-	return &EndpointPickerConfigApplyConfiguration{}
-}
-
-// WithExtensionRef sets the ExtensionRef 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 ExtensionRef field is set to the value of the last call.
-func (b *EndpointPickerConfigApplyConfiguration) WithExtensionRef(value *ExtensionApplyConfiguration) *EndpointPickerConfigApplyConfiguration {
-	b.ExtensionRef = value
-	return b
-}
diff --git a/client-go/applyconfiguration/apix/v1alpha2/extension.go b/client-go/applyconfiguration/apix/v1alpha2/extension.go
index e3690f9..2f40a61 100644
--- a/client-go/applyconfiguration/apix/v1alpha2/extension.go
+++ b/client-go/applyconfiguration/apix/v1alpha2/extension.go
@@ -25,8 +25,11 @@ import (
 // ExtensionApplyConfiguration represents a declarative configuration of the Extension type for use
 // with apply.
 type ExtensionApplyConfiguration struct {
-	ExtensionReferenceApplyConfiguration  `json:",inline"`
-	ExtensionConnectionApplyConfiguration `json:",inline"`
+	Group       *apixv1alpha2.Group                `json:"group,omitempty"`
+	Kind        *apixv1alpha2.Kind                 `json:"kind,omitempty"`
+	Name        *apixv1alpha2.ObjectName           `json:"name,omitempty"`
+	PortNumber  *apixv1alpha2.PortNumber           `json:"portNumber,omitempty"`
+	FailureMode *apixv1alpha2.ExtensionFailureMode `json:"failureMode,omitempty"`
 }
 
 // ExtensionApplyConfiguration constructs a declarative configuration of the Extension type for use with
@@ -39,7 +42,7 @@ func Extension() *ExtensionApplyConfiguration {
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the Group field is set to the value of the last call.
 func (b *ExtensionApplyConfiguration) WithGroup(value apixv1alpha2.Group) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.Group = &value
+	b.Group = &value
 	return b
 }
 
@@ -47,7 +50,7 @@ func (b *ExtensionApplyConfiguration) WithGroup(value apixv1alpha2.Group) *Exten
 // 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 *ExtensionApplyConfiguration) WithKind(value apixv1alpha2.Kind) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.Kind = &value
+	b.Kind = &value
 	return b
 }
 
@@ -55,7 +58,7 @@ func (b *ExtensionApplyConfiguration) WithKind(value apixv1alpha2.Kind) *Extensi
 // 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 *ExtensionApplyConfiguration) WithName(value apixv1alpha2.ObjectName) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.Name = &value
+	b.Name = &value
 	return b
 }
 
@@ -63,7 +66,7 @@ func (b *ExtensionApplyConfiguration) WithName(value apixv1alpha2.ObjectName) *E
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the PortNumber field is set to the value of the last call.
 func (b *ExtensionApplyConfiguration) WithPortNumber(value apixv1alpha2.PortNumber) *ExtensionApplyConfiguration {
-	b.ExtensionReferenceApplyConfiguration.PortNumber = &value
+	b.PortNumber = &value
 	return b
 }
 
@@ -71,6 +74,6 @@ func (b *ExtensionApplyConfiguration) WithPortNumber(value apixv1alpha2.PortNumb
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the FailureMode field is set to the value of the last call.
 func (b *ExtensionApplyConfiguration) WithFailureMode(value apixv1alpha2.ExtensionFailureMode) *ExtensionApplyConfiguration {
-	b.ExtensionConnectionApplyConfiguration.FailureMode = &value
+	b.FailureMode = &value
 	return b
 }
diff --git a/client-go/applyconfiguration/apix/v1alpha2/extensionconnection.go b/client-go/applyconfiguration/apix/v1alpha2/extensionconnection.go
deleted file mode 100644
index 17c1c6f..0000000
--- a/client-go/applyconfiguration/apix/v1alpha2/extensionconnection.go
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
-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 v1alpha2
-
-import (
-	apixv1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
-)
-
-// ExtensionConnectionApplyConfiguration represents a declarative configuration of the ExtensionConnection type for use
-// with apply.
-type ExtensionConnectionApplyConfiguration struct {
-	FailureMode *apixv1alpha2.ExtensionFailureMode `json:"failureMode,omitempty"`
-}
-
-// ExtensionConnectionApplyConfiguration constructs a declarative configuration of the ExtensionConnection type for use with
-// apply.
-func ExtensionConnection() *ExtensionConnectionApplyConfiguration {
-	return &ExtensionConnectionApplyConfiguration{}
-}
-
-// WithFailureMode sets the FailureMode 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 FailureMode field is set to the value of the last call.
-func (b *ExtensionConnectionApplyConfiguration) WithFailureMode(value apixv1alpha2.ExtensionFailureMode) *ExtensionConnectionApplyConfiguration {
-	b.FailureMode = &value
-	return b
-}
diff --git a/client-go/applyconfiguration/apix/v1alpha2/extensionreference.go b/client-go/applyconfiguration/apix/v1alpha2/extensionreference.go
deleted file mode 100644
index 1142cf2..0000000
--- a/client-go/applyconfiguration/apix/v1alpha2/extensionreference.go
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-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 v1alpha2
-
-import (
-	apixv1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
-)
-
-// ExtensionReferenceApplyConfiguration represents a declarative configuration of the ExtensionReference type for use
-// with apply.
-type ExtensionReferenceApplyConfiguration struct {
-	Group      *apixv1alpha2.Group      `json:"group,omitempty"`
-	Kind       *apixv1alpha2.Kind       `json:"kind,omitempty"`
-	Name       *apixv1alpha2.ObjectName `json:"name,omitempty"`
-	PortNumber *apixv1alpha2.PortNumber `json:"portNumber,omitempty"`
-}
-
-// ExtensionReferenceApplyConfiguration constructs a declarative configuration of the ExtensionReference type for use with
-// apply.
-func ExtensionReference() *ExtensionReferenceApplyConfiguration {
-	return &ExtensionReferenceApplyConfiguration{}
-}
-
-// WithGroup sets the Group 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 Group field is set to the value of the last call.
-func (b *ExtensionReferenceApplyConfiguration) WithGroup(value apixv1alpha2.Group) *ExtensionReferenceApplyConfiguration {
-	b.Group = &value
-	return b
-}
-
-// 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 *ExtensionReferenceApplyConfiguration) WithKind(value apixv1alpha2.Kind) *ExtensionReferenceApplyConfiguration {
-	b.Kind = &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 *ExtensionReferenceApplyConfiguration) WithName(value apixv1alpha2.ObjectName) *ExtensionReferenceApplyConfiguration {
-	b.Name = &value
-	return b
-}
-
-// WithPortNumber sets the PortNumber 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 PortNumber field is set to the value of the last call.
-func (b *ExtensionReferenceApplyConfiguration) WithPortNumber(value apixv1alpha2.PortNumber) *ExtensionReferenceApplyConfiguration {
-	b.PortNumber = &value
-	return b
-}
diff --git a/client-go/applyconfiguration/apix/v1alpha2/inferenceobjectivespec.go b/client-go/applyconfiguration/apix/v1alpha2/inferenceobjectivespec.go
index a8554a7..6fbcf33 100644
--- a/client-go/applyconfiguration/apix/v1alpha2/inferenceobjectivespec.go
+++ b/client-go/applyconfiguration/apix/v1alpha2/inferenceobjectivespec.go
@@ -18,16 +18,11 @@ limitations under the License.
 
 package v1alpha2
 
-import (
-	apixv1alpha2 "sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
-)
-
 // InferenceObjectiveSpecApplyConfiguration represents a declarative configuration of the InferenceObjectiveSpec type for use
 // with apply.
 type InferenceObjectiveSpecApplyConfiguration struct {
-	Criticality  *apixv1alpha2.Criticality              `json:"criticality,omitempty"`
-	TargetModels []TargetModelApplyConfiguration        `json:"targetModels,omitempty"`
-	PoolRef      *PoolObjectReferenceApplyConfiguration `json:"poolRef,omitempty"`
+	Priority *int                                   `json:"priority,omitempty"`
+	PoolRef  *PoolObjectReferenceApplyConfiguration `json:"poolRef,omitempty"`
 }
 
 // InferenceObjectiveSpecApplyConfiguration constructs a declarative configuration of the InferenceObjectiveSpec type for use with
@@ -36,24 +31,11 @@ func InferenceObjectiveSpec() *InferenceObjectiveSpecApplyConfiguration {
 	return &InferenceObjectiveSpecApplyConfiguration{}
 }
 
-// WithCriticality sets the Criticality field in the declarative configuration to the given value
+// WithPriority sets the Priority 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 Criticality field is set to the value of the last call.
-func (b *InferenceObjectiveSpecApplyConfiguration) WithCriticality(value apixv1alpha2.Criticality) *InferenceObjectiveSpecApplyConfiguration {
-	b.Criticality = &value
-	return b
-}
-
-// WithTargetModels adds the given value to the TargetModels 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 TargetModels field.
-func (b *InferenceObjectiveSpecApplyConfiguration) WithTargetModels(values ...*TargetModelApplyConfiguration) *InferenceObjectiveSpecApplyConfiguration {
-	for i := range values {
-		if values[i] == nil {
-			panic("nil value passed to WithTargetModels")
-		}
-		b.TargetModels = append(b.TargetModels, *values[i])
-	}
+// If called multiple times, the Priority field is set to the value of the last call.
+func (b *InferenceObjectiveSpecApplyConfiguration) WithPriority(value int) *InferenceObjectiveSpecApplyConfiguration {
+	b.Priority = &value
 	return b
 }
 
diff --git a/client-go/applyconfiguration/apix/v1alpha2/inferencepoolspec.go b/client-go/applyconfiguration/apix/v1alpha2/inferencepoolspec.go
index 8773d9c..1f59e88 100644
--- a/client-go/applyconfiguration/apix/v1alpha2/inferencepoolspec.go
+++ b/client-go/applyconfiguration/apix/v1alpha2/inferencepoolspec.go
@@ -25,9 +25,9 @@ import (
 // InferencePoolSpecApplyConfiguration represents a declarative configuration of the InferencePoolSpec type for use
 // with apply.
 type InferencePoolSpecApplyConfiguration struct {
-	Selector                               map[apixv1alpha2.LabelKey]apixv1alpha2.LabelValue `json:"selector,omitempty"`
-	TargetPortNumber                       *int32                                            `json:"targetPortNumber,omitempty"`
-	EndpointPickerConfigApplyConfiguration `json:",inline"`
+	Selector         map[apixv1alpha2.LabelKey]apixv1alpha2.LabelValue `json:"selector,omitempty"`
+	TargetPortNumber *int32                                            `json:"targetPortNumber,omitempty"`
+	ExtensionRef     *ExtensionApplyConfiguration                      `json:"extensionRef,omitempty"`
 }
 
 // InferencePoolSpecApplyConfiguration constructs a declarative configuration of the InferencePoolSpec type for use with
@@ -62,6 +62,6 @@ func (b *InferencePoolSpecApplyConfiguration) WithTargetPortNumber(value int32)
 // and returns the receiver, so that objects can be built by chaining "With" function invocations.
 // If called multiple times, the ExtensionRef field is set to the value of the last call.
 func (b *InferencePoolSpecApplyConfiguration) WithExtensionRef(value *ExtensionApplyConfiguration) *InferencePoolSpecApplyConfiguration {
-	b.EndpointPickerConfigApplyConfiguration.ExtensionRef = value
+	b.ExtensionRef = value
 	return b
 }
diff --git a/client-go/applyconfiguration/apix/v1alpha2/targetmodel.go b/client-go/applyconfiguration/apix/v1alpha2/targetmodel.go
deleted file mode 100644
index 643587d..0000000
--- a/client-go/applyconfiguration/apix/v1alpha2/targetmodel.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-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 v1alpha2
-
-// TargetModelApplyConfiguration represents a declarative configuration of the TargetModel type for use
-// with apply.
-type TargetModelApplyConfiguration struct {
-	Name   *string `json:"name,omitempty"`
-	Weight *int32  `json:"weight,omitempty"`
-}
-
-// TargetModelApplyConfiguration constructs a declarative configuration of the TargetModel type for use with
-// apply.
-func TargetModel() *TargetModelApplyConfiguration {
-	return &TargetModelApplyConfiguration{}
-}
-
-// 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 *TargetModelApplyConfiguration) WithName(value string) *TargetModelApplyConfiguration {
-	b.Name = &value
-	return b
-}
-
-// WithWeight sets the Weight 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 Weight field is set to the value of the last call.
-func (b *TargetModelApplyConfiguration) WithWeight(value int32) *TargetModelApplyConfiguration {
-	b.Weight = &value
-	return b
-}
diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go
index 46895d8..c5d4f57 100644
--- a/client-go/applyconfiguration/utils.go
+++ b/client-go/applyconfiguration/utils.go
@@ -34,34 +34,26 @@ import (
 func ForKind(kind schema.GroupVersionKind) interface{} {
 	switch kind {
 	// Group=inference.networking.k8s.io, Version=v1
-	case v1.SchemeGroupVersion.WithKind("EndpointPickerConfig"):
-		return &apiv1.EndpointPickerConfigApplyConfiguration{}
 	case v1.SchemeGroupVersion.WithKind("Extension"):
 		return &apiv1.ExtensionApplyConfiguration{}
-	case v1.SchemeGroupVersion.WithKind("ExtensionConnection"):
-		return &apiv1.ExtensionConnectionApplyConfiguration{}
-	case v1.SchemeGroupVersion.WithKind("ExtensionReference"):
-		return &apiv1.ExtensionReferenceApplyConfiguration{}
 	case v1.SchemeGroupVersion.WithKind("InferencePool"):
 		return &apiv1.InferencePoolApplyConfiguration{}
 	case v1.SchemeGroupVersion.WithKind("InferencePoolSpec"):
 		return &apiv1.InferencePoolSpecApplyConfiguration{}
 	case v1.SchemeGroupVersion.WithKind("InferencePoolStatus"):
 		return &apiv1.InferencePoolStatusApplyConfiguration{}
+	case v1.SchemeGroupVersion.WithKind("LabelSelector"):
+		return &apiv1.LabelSelectorApplyConfiguration{}
 	case v1.SchemeGroupVersion.WithKind("ParentGatewayReference"):
 		return &apiv1.ParentGatewayReferenceApplyConfiguration{}
 	case v1.SchemeGroupVersion.WithKind("PoolStatus"):
 		return &apiv1.PoolStatusApplyConfiguration{}
+	case v1.SchemeGroupVersion.WithKind("Port"):
+		return &apiv1.PortApplyConfiguration{}
 
 		// Group=inference.networking.x-k8s.io, Version=v1alpha2
-	case v1alpha2.SchemeGroupVersion.WithKind("EndpointPickerConfig"):
-		return &apixv1alpha2.EndpointPickerConfigApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("Extension"):
 		return &apixv1alpha2.ExtensionApplyConfiguration{}
-	case v1alpha2.SchemeGroupVersion.WithKind("ExtensionConnection"):
-		return &apixv1alpha2.ExtensionConnectionApplyConfiguration{}
-	case v1alpha2.SchemeGroupVersion.WithKind("ExtensionReference"):
-		return &apixv1alpha2.ExtensionReferenceApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("InferenceObjective"):
 		return &apixv1alpha2.InferenceObjectiveApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("InferenceObjectiveSpec"):
@@ -80,8 +72,6 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
 		return &apixv1alpha2.PoolObjectReferenceApplyConfiguration{}
 	case v1alpha2.SchemeGroupVersion.WithKind("PoolStatus"):
 		return &apixv1alpha2.PoolStatusApplyConfiguration{}
-	case v1alpha2.SchemeGroupVersion.WithKind("TargetModel"):
-		return &apixv1alpha2.TargetModelApplyConfiguration{}
 
 	}
 	return nil
diff --git a/cmd/epp/runner/health.go b/cmd/epp/runner/health.go
index cc5da8d..6e63e17 100644
--- a/cmd/epp/runner/health.go
+++ b/cmd/epp/runner/health.go
@@ -18,6 +18,8 @@ package runner
 
 import (
 	"context"
+	"fmt"
+	"sync/atomic"
 
 	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
 	"github.com/go-logr/logr"
@@ -30,37 +32,83 @@ import (
 )
 
 type healthServer struct {
-	logger    logr.Logger
-	datastore datastore.Datastore
+	logger                logr.Logger
+	datastore             datastore.Datastore
+	isLeader              *atomic.Bool
+	leaderElectionEnabled bool
 }
 
+const (
+	LivenessCheckService  = "liveness"
+	ReadinessCheckService = "readiness"
+)
+
 func (s *healthServer) Check(ctx context.Context, in *healthPb.HealthCheckRequest) (*healthPb.HealthCheckResponse, error) {
-	// TODO: we're accepting ANY service name for now as a temporary hack in alignment with
-	// upstream issues. See https://github.com/kubernetes-sigs/gateway-api-inference-extension/pull/788
-	// if in.Service != extProcPb.ExternalProcessor_ServiceDesc.ServiceName {
-	// 	s.logger.V(logutil.DEFAULT).Info("gRPC health check requested unknown service", "available-services", []string{extProcPb.ExternalProcessor_ServiceDesc.ServiceName}, "requested-service", in.Service)
-	// 	return &healthPb.HealthCheckResponse{Status: healthPb.HealthCheckResponse_SERVICE_UNKNOWN}, nil
-	// }
-
-	if !s.datastore.PoolHasSynced() {
-		s.logger.V(logutil.DEFAULT).Info("gRPC health check not serving", "service", in.Service)
+	isLive := s.datastore.PoolHasSynced()
+
+	// If leader election is disabled, use current logic: all checks are based on whether the pool has synced.
+	if !s.leaderElectionEnabled {
+		if !isLive {
+			s.logger.V(logutil.DEFAULT).Info("gRPC health check not serving (leader election disabled)", "service", in.Service)
+			return &healthPb.HealthCheckResponse{Status: healthPb.HealthCheckResponse_NOT_SERVING}, nil
+		}
+		s.logger.V(logutil.TRACE).Info("gRPC health check serving (leader election disabled)", "service", in.Service)
+		return &healthPb.HealthCheckResponse{Status: healthPb.HealthCheckResponse_SERVING}, nil
+	}
+
+	// When leader election is enabled, differentiate between liveness and readiness.
+	// The service name in the request determines which check to perform.
+	var checkName string
+	var isPassing bool
+
+	switch in.Service {
+	case ReadinessCheckService:
+		checkName = "readiness"
+		isPassing = isLive && s.isLeader.Load()
+	case LivenessCheckService, "": // Default to liveness check if service is empty
+		checkName = "liveness"
+		// Any pod that is running and can respond to this gRPC check is considered "live".
+		// The datastore sync status should not affect liveness, only readiness.
+		// This is to prevent the non-leader node from continuous restarts
+		isPassing = true
+	case extProcPb.ExternalProcessor_ServiceDesc.ServiceName:
+		// The main service is considered ready only on the leader.
+		checkName = "ext_proc"
+		isPassing = isLive && s.isLeader.Load()
+	default:
+		s.logger.V(logutil.DEFAULT).Info("gRPC health check requested unknown service", "available-services", []string{LivenessCheckService, ReadinessCheckService, extProcPb.ExternalProcessor_ServiceDesc.ServiceName}, "requested-service", in.Service)
+		return &healthPb.HealthCheckResponse{Status: healthPb.HealthCheckResponse_SERVICE_UNKNOWN}, nil
+	}
+
+	if !isPassing {
+		s.logger.V(logutil.DEFAULT).Info(fmt.Sprintf("gRPC %s check not serving", checkName), "service", in.Service, "isLive", isLive, "isLeader", s.isLeader.Load())
 		return &healthPb.HealthCheckResponse{Status: healthPb.HealthCheckResponse_NOT_SERVING}, nil
 	}
-	s.logger.V(logutil.TRACE).Info("gRPC health check serving", "service", in.Service)
+
+	s.logger.V(logutil.TRACE).Info(fmt.Sprintf("gRPC %s check serving", checkName), "service", in.Service)
 	return &healthPb.HealthCheckResponse{Status: healthPb.HealthCheckResponse_SERVING}, nil
 }
 
 func (s *healthServer) List(ctx context.Context, _ *healthPb.HealthListRequest) (*healthPb.HealthListResponse, error) {
-	// currently only the ext_proc service is provided
-	serviceHealthResponse, err := s.Check(ctx, &healthPb.HealthCheckRequest{Service: extProcPb.ExternalProcessor_ServiceDesc.ServiceName})
-	if err != nil {
-		return nil, err
+	statuses := make(map[string]*healthPb.HealthCheckResponse)
+
+	services := []string{extProcPb.ExternalProcessor_ServiceDesc.ServiceName}
+	if s.leaderElectionEnabled {
+		services = append(services, LivenessCheckService, ReadinessCheckService)
+	}
+
+	for _, service := range services {
+		resp, err := s.Check(ctx, &healthPb.HealthCheckRequest{Service: service})
+		if err != nil {
+			// Check can return an error for unknown services, but here we are iterating known services.
+			// If another error occurs, we should probably return it.
+			return nil, err
+		}
+		statuses[service] = resp
 	}
 
 	return &healthPb.HealthListResponse{
-		Statuses: map[string]*healthPb.HealthCheckResponse{
-			extProcPb.ExternalProcessor_ServiceDesc.ServiceName: serviceHealthResponse,
-		},
+		Statuses: statuses,
 	}, nil
 }
 
diff --git a/cmd/epp/runner/runner.go b/cmd/epp/runner/runner.go
index d8e21c4..eb2ea3b 100644
--- a/cmd/epp/runner/runner.go
+++ b/cmd/epp/runner/runner.go
@@ -25,6 +25,7 @@ import (
 	"net/http"
 	"net/http/pprof"
 	"os"
+	"sync/atomic"
 
 	"github.com/go-logr/logr"
 	"github.com/prometheus/client_golang/prometheus"
@@ -147,10 +148,14 @@ var (
 		"The configuration specified as text, in lieu of a file")
 
 	modelServerMetricsPort = flag.Int("model-server-metrics-port", 0, "Port to scrape metrics from pods. "+
-		"Default value will be set to InferencePool.Spec.TargetPortNumber if not set.")
+		"Default value will be set to the InferencePool.Spec.TargetPorts[0].Number if not set.")
 	modelServerMetricsPath                    = flag.String("model-server-metrics-path", "/metrics", "Path to scrape metrics from pods")
 	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.")
 
 	setupLog = ctrl.Log.WithName("setup")
 )
@@ -189,9 +194,11 @@ func bindEnvToFlags() {
 		"MODEL_SERVER_METRICS_HTTPS_INSECURE_SKIP_VERIFY": "model-server-metrics-https-insecure-skip-verify",
 		"POOL_NAME":                                       "pool-name",
 		"POOL_NAMESPACE":                                  "pool-namespace",
+		"POOL_GROUP":                                      "pool-group",
 		// durations & bools work too; flag.Set expects the *string* form
-		"REFRESH_METRICS_INTERVAL": "refresh-metrics-interval",
-		"SECURE_SERVING":           "secure-serving",
+		"REFRESH_METRICS_INTERVAL":  "refresh-metrics-interval",
+		"SECURE_SERVING":            "secure-serving",
+		"HA_ENABLE_LEADER_ELECTION": "ha-enable-leader-election",
 	} {
 		if v := os.Getenv(env); v != "" {
 			// ignore error; Parse() will catch invalid values later
@@ -269,7 +276,7 @@ func (r *Runner) Run(ctx context.Context) error {
 		ModelServerMetricsScheme: *modelServerMetricsScheme,
 		Client:                   metricsHttpClient,
 	},
-		*refreshMetricsInterval, *metricsStalenessThreshold)
+		*refreshMetricsInterval)
 
 	datastore := datastore.NewDatastore(ctx, pmf)
 
@@ -299,12 +306,28 @@ func (r *Runner) Run(ctx context.Context) error {
 		NamespacedName: poolNamespacedName,
 		GroupKind:      poolGroupKind,
 	}
-	mgr, err := runserver.NewDefaultManager(poolGKNN, cfg, metricsServerOptions)
+
+	isLeader := &atomic.Bool{}
+	isLeader.Store(false)
+
+	mgr, err := runserver.NewDefaultManager(poolGKNN, cfg, metricsServerOptions, *haEnableLeaderElection)
 	if err != nil {
 		setupLog.Error(err, "Failed to create controller manager")
 		return err
 	}
 
+	if *haEnableLeaderElection {
+		setupLog.Info("Leader election enabled")
+		go func() {
+			<-mgr.Elected()
+			isLeader.Store(true)
+			setupLog.Info("This instance is now the leader!")
+		}()
+	} else {
+		// If leader election is disabled, all instances are "leaders" for readiness purposes.
+		isLeader.Store(true)
+	}
+
 	if *enablePprof {
 		setupLog.Info("Enabling pprof handlers")
 		err = setupPprofHandlers(mgr)
@@ -356,7 +379,7 @@ func (r *Runner) Run(ctx context.Context) error {
 
 	// --- Add Runnables to Manager ---
 	// Register health server.
-	if err := registerHealthServer(mgr, ctrl.Log.WithName("health"), datastore, *grpcHealthPort); err != nil {
+	if err := registerHealthServer(mgr, ctrl.Log.WithName("health"), datastore, *grpcHealthPort, isLeader, *haEnableLeaderElection); err != nil {
 		return err
 	}
 
@@ -452,11 +475,13 @@ func registerExtProcServer(mgr manager.Manager, runner *runserver.ExtProcServerR
 }
 
 // registerHealthServer adds the Health gRPC server as a Runnable to the given manager.
-func registerHealthServer(mgr manager.Manager, logger logr.Logger, ds datastore.Datastore, port int) error {
+func registerHealthServer(mgr manager.Manager, logger logr.Logger, ds datastore.Datastore, port int, isLeader *atomic.Bool, leaderElectionEnabled bool) error {
 	srv := grpc.NewServer()
 	healthPb.RegisterHealthServer(srv, &healthServer{
-		logger:    logger,
-		datastore: ds,
+		logger:                logger,
+		datastore:             ds,
+		isLeader:              isLeader,
+		leaderElectionEnabled: leaderElectionEnabled,
 	})
 	if err := mgr.Add(
 		runnable.NoLeaderElection(runnable.GRPCServer("health", srv, port))); err != nil {
diff --git a/config/charts/inferencepool/README.md b/config/charts/inferencepool/README.md
index b8a8d0a..edc01d2 100644
--- a/config/charts/inferencepool/README.md
+++ b/config/charts/inferencepool/README.md
@@ -79,6 +79,24 @@ $ helm install triton-llama3-8b-instruct \
   oci://us-central1-docker.pkg.dev/k8s-staging-images/gateway-api-inference-extension/charts/inferencepool --version v0
 ```
 
+### Install with High Availability (HA)
+
+To deploy the EndpointPicker in a high-availability (HA) active-passive configuration, you can enable leader election. When enabled, the EPP deployment will have multiple replicas, but only one "leader" replica will be active and ready to process traffic at any given time. If the leader pod fails, another pod will be elected as the new leader, ensuring service continuity.
+
+To enable HA, set `inferenceExtension.enableLeaderElection` to `true` and increase the number of replicas in your `values.yaml` file:
+
+```yaml
+inferenceExtension:
+  replicas: 3
+  enableLeaderElection: true
+```
+
+Then apply it with:
+
+```txt
+helm install vllm-llama3-8b-instruct ./config/charts/inferencepool -f values.yaml \
+```
+
 ## Uninstall
 
 Run the following command to uninstall the chart:
@@ -107,6 +125,8 @@ The following table list the configurable parameters of the chart.
 | `inferenceExtension.extraServicePorts`      | List of additional service ports to expose. Defaults to `[]`.                                                         |
 | `inferenceExtension.logVerbosity`           | Logging verbosity level for the endpoint picker. Defaults to `"3"`.                                                   |
 | `provider.name`                             | Name of the Inference Gateway implementation being used. Possible values: `gke`. Defaults to `none`.                   |
+| `inferenceExtension.enableLeaderElection`   | Enable leader election for high availability. When enabled, only one EPP pod (the leader) will be ready to serve traffic. It is recommended to set `inferenceExtension.replicas` to a value greater than 1 when this is set to `true`. Defaults to `false`. |
+
 
 ## Notes
 
diff --git a/config/charts/inferencepool/templates/epp-deployment.yaml b/config/charts/inferencepool/templates/epp-deployment.yaml
index 23957a3..fc15449 100644
--- a/config/charts/inferencepool/templates/epp-deployment.yaml
+++ b/config/charts/inferencepool/templates/epp-deployment.yaml
@@ -44,6 +44,9 @@ spec:
         - "--model-server-metrics-path={{ .Values.inferenceExtension.modelServerMetricsPath }}"
         - "--model-server-metrics-scheme={{ .Values.inferenceExtension.modelServerMetricsScheme }}"
         - "--model-server-metrics-https-insecure-skip-verify={{ .Values.inferenceExtension.modelServerMetricsHttpsInsecureSkipVerify }}"
+        {{- if .Values.inferenceExtension.enableLeaderElection }}
+        - "--ha-enable-leader-election"
+        {{- end }}
         {{- if eq (.Values.inferencePool.modelServerType | default "vllm") "triton-tensorrt-llm" }}
         - --total-queued-requests-metric
         - "nv_trt_llm_request_metrics{request_type=waiting}"
@@ -63,15 +66,27 @@ spec:
         {{- toYaml . | nindent 8 }}
         {{- end }}
         livenessProbe:
+          {{- if .Values.inferenceExtension.enableLeaderElection }}
+          grpc:
+            port: 9003
+            service: liveness
+          {{- else }}
           grpc:
             port: 9003
             service: inference-extension
+          {{- end }}
           initialDelaySeconds: 5
           periodSeconds: 10
         readinessProbe:
+          {{- if .Values.inferenceExtension.enableLeaderElection }}
+          grpc:
+            port: 9003
+            service: readiness
+          {{- else }}
           grpc:
             port: 9003
             service: inference-extension
+          {{- end }}
           initialDelaySeconds: 5
           periodSeconds: 10
         {{- with .Values.inferenceExtension.env }}
diff --git a/config/charts/inferencepool/templates/inferencepool.yaml b/config/charts/inferencepool/templates/inferencepool.yaml
index f9d04df..ad13f2d 100644
--- a/config/charts/inferencepool/templates/inferencepool.yaml
+++ b/config/charts/inferencepool/templates/inferencepool.yaml
@@ -7,12 +7,16 @@ metadata:
   labels:
     {{- include "gateway-api-inference-extension.labels" . | nindent 4 }}
 spec:
-  targetPortNumber: {{ .Values.inferencePool.targetPortNumber }}
-  selector:
-    {{- if .Values.inferencePool.modelServers.matchLabels }}
-    {{- range $key, $value := .Values.inferencePool.modelServers.matchLabels }}
-    {{ $key }}: {{ quote $value }}
-    {{- end }}
+  targetPorts:
+    {{- range .Values.inferencePool.targetPorts }}
+      - number: {{ .number }}
     {{- end }}
+  selector:
+    matchLabels:
+      {{- if .Values.inferencePool.modelServers.matchLabels }}
+      {{- range $key, $value := .Values.inferencePool.modelServers.matchLabels }}
+      {{ $key }}: {{ quote $value }}
+      {{- end }}
+      {{- end }}
   extensionRef:
     name: {{ include "gateway-api-inference-extension.name" . }}
diff --git a/config/charts/inferencepool/templates/leader-election-rbac.yaml b/config/charts/inferencepool/templates/leader-election-rbac.yaml
new file mode 100644
index 0000000..923bdd6
--- /dev/null
+++ b/config/charts/inferencepool/templates/leader-election-rbac.yaml
@@ -0,0 +1,30 @@
+{{- if .Values.inferenceExtension.enableLeaderElection }}
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: {{ include "gateway-api-inference-extension.name" . }}-leader-election
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "gateway-api-inference-extension.labels" . | nindent 4 }}
+rules:
+- apiGroups: [ "coordination.k8s.io" ]
+  resources: [ "leases" ]
+  verbs: [ "get", "list", "watch", "create", "update", "patch", "delete" ]
+- apiGroups: [ "" ]
+  resources: [ "events" ]
+  verbs: [ "create", "patch" ]
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: {{ include "gateway-api-inference-extension.name" . }}-leader-election-binding
+  namespace: {{ .Release.Namespace }}
+subjects:
+- kind: ServiceAccount
+  name: {{ include "gateway-api-inference-extension.name" . }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ include "gateway-api-inference-extension.name" . }}-leader-election
+{{- end }}
diff --git a/config/charts/inferencepool/values.yaml b/config/charts/inferencepool/values.yaml
index 2c295c1..5f0c2fd 100644
--- a/config/charts/inferencepool/values.yaml
+++ b/config/charts/inferencepool/values.yaml
@@ -34,13 +34,17 @@ inferenceExtension:
   extraContainerPorts: []
   # Define additional service ports
   extraServicePorts: []
+  # Enable leader election for high availability. When enabled, it is recommended to set replicas > 1.
+  # Only the leader pod will be ready to serve traffic.
+  enableLeaderElection: false
 
 inferencePool:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   modelServerType: vllm # vllm, triton-tensorrt-llm
-  # modelServers: # REQUIRED
-    # matchLabels: 
-    #   app: vllm-llama3-8b-instruct
+  modelServers: # REQUIRED
+    matchLabels:
+      app: vllm-llama3-8b-instruct
 
 provider:
   name: none
diff --git a/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml b/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml
index 78e3a9c..92072eb 100644
--- a/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml
+++ b/config/crd/bases/inference.networking.k8s.io_inferencepools.yaml
@@ -12,6 +12,8 @@ spec:
     kind: InferencePool
     listKind: InferencePoolList
     plural: inferencepools
+    shortNames:
+    - infpool
     singular: inferencepool
   scope: Namespaced
   versions:
@@ -60,6 +62,7 @@ spec:
                       Group is the group of the referent.
                       The default value is "", representing the Core 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:
@@ -96,43 +99,70 @@ spec:
                 - name
                 type: object
               selector:
-                additionalProperties:
-                  description: |-
-                    LabelValue is the value of a label. This is used for validation
-                    of maps. This matches the Kubernetes label validation rules:
-                    * must be 63 characters or less (can be empty),
-                    * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]),
-                    * could contain dashes (-), underscores (_), dots (.), and alphanumerics between.
+                description: |-
+                  Selector determines which Pods are members of this inference pool.
+                  It matches Pods by their labels only within the same namespace; cross-namespace
+                  selection is not supported.
 
-                    Valid values include:
+                  The structure of this LabelSelector is intentionally simple to be compatible
+                  with Kubernetes Service selectors, as some implementations may translate
+                  this configuration into a Service resource.
+                properties:
+                  matchLabels:
+                    additionalProperties:
+                      description: |-
+                        LabelValue is the value of a label. This is used for validation
+                        of maps. This matches the Kubernetes label validation rules:
+                        * must be 63 characters or less (can be empty),
+                        * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]),
+                        * could contain dashes (-), underscores (_), dots (.), and alphanumerics between.
 
-                    * MyValue
-                    * my.name
-                    * 123-my-value
-                  maxLength: 63
-                  minLength: 0
-                  pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$
-                  type: string
-                description: |-
-                  Selector defines a map of labels to watch model server Pods
-                  that should be included in the InferencePool.
-                  In some cases, implementations may translate this field to a Service selector, so this matches the simple
-                  map used for Service selectors instead of the full Kubernetes LabelSelector type.
-                  If specified, it will be applied to match the model server pods in the same namespace as the InferencePool.
-                  Cross namesoace selector is not supported.
+                        Valid values include:
+
+                        * MyValue
+                        * my.name
+                        * 123-my-value
+                      maxLength: 63
+                      minLength: 0
+                      pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$
+                      type: string
+                    description: |-
+                      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.
+                    type: object
+                required:
+                - matchLabels
                 type: object
-              targetPortNumber:
+              targetPorts:
                 description: |-
-                  TargetPortNumber defines the port number to access the selected model server Pods.
-                  The number must be in the range 1 to 65535.
-                format: int32
-                maximum: 65535
-                minimum: 1
-                type: integer
+                  TargetPorts defines a list of ports that are exposed by this InferencePool.
+                  Currently, the list may only include a single port definition.
+                items:
+                  description: Port defines the network port that will be exposed
+                    by this InferencePool.
+                  properties:
+                    number:
+                      description: |-
+                        Number defines the port number to access the selected model server Pods.
+                        The number must be in the range 1 to 65535.
+                      format: int32
+                      maximum: 65535
+                      minimum: 1
+                      type: integer
+                  required:
+                  - number
+                  type: object
+                maxItems: 1
+                minItems: 1
+                type: array
+                x-kubernetes-list-map-keys:
+                - number
+                x-kubernetes-list-type: map
             required:
             - extensionRef
             - selector
-            - targetPortNumber
+            - targetPorts
             type: object
           status:
             default:
@@ -147,6 +177,7 @@ spec:
                   kind: Status
                   name: default
             description: Status defines the observed state of InferencePool.
+            minProperties: 1
             properties:
               parent:
                 description: |-
@@ -247,6 +278,7 @@ spec:
                           default: gateway.networking.k8s.io
                           description: Group is the group of the referent.
                           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:
@@ -278,7 +310,10 @@ spec:
                   type: object
                 maxItems: 32
                 type: array
+                x-kubernetes-list-type: atomic
             type: object
+        required:
+        - spec
         type: object
     served: true
     storage: true
diff --git a/config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml b/config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml
index 1c86d0a..7f0968f 100644
--- a/config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml
+++ b/config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml
@@ -21,8 +21,8 @@ spec:
     - jsonPath: .spec.poolRef.name
       name: Inference Pool
       type: string
-    - jsonPath: .spec.criticality
-      name: Criticality
+    - jsonPath: .spec.priority
+      name: Priority
       type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
@@ -68,27 +68,12 @@ spec:
               creation timestamp, will be selected to remain valid. In the event of a race
               condition, one will be selected at random.
             properties:
-              criticality:
-                description: |-
-                  Criticality defines how important it is to serve the model compared to other models referencing the same pool.
-                  Criticality impacts how traffic is handled in resource constrained situations. It handles this by
-                  queuing or rejecting requests of lower criticality. InferenceObjectives of an equivalent Criticality will
-                  fairly share resources over throughput of tokens. In the future, the metric used to calculate fairness,
-                  and the proportionality of fairness will be configurable.
-
-                  Default values for this field will not be set, to allow for future additions of new field that may 'one of' with this field.
-                  Any implementations that may consume this field may treat an unset value as the 'Standard' range.
-                enum:
-                - Critical
-                - Standard
-                - Sheddable
-                type: string
               poolRef:
                 description: PoolRef is a reference to the inference pool, the pool
                   must exist in the same namespace.
                 properties:
                   group:
-                    default: inference.networking.x-k8s.io
+                    default: inference.networking.k8s.io
                     description: Group is the group of the referent.
                     maxLength: 253
                     pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
@@ -108,52 +93,21 @@ spec:
                 required:
                 - name
                 type: object
-              targetModels:
+              priority:
                 description: |-
-                  TargetModels allow multiple versions of a model for traffic splitting.
-                  If not specified, the target model name is defaulted to the modelName parameter.
-                  modelName is often in reference to a LoRA adapter.
-                items:
-                  description: |-
-                    TargetModel represents a deployed model or a LoRA adapter. The
-                    Name field is expected to match the name of the LoRA adapter
-                    (or base model) as it is registered within the model server. Inference
-                    Gateway assumes that the model exists on the model server and it's the
-                    responsibility of the user to validate a correct match. Should a model fail
-                    to exist at request time, the error is processed by the Inference Gateway
-                    and emitted on the appropriate InferenceObjective object.
-                  properties:
-                    name:
-                      description: Name is the name of the adapter or base model,
-                        as expected by the ModelServer.
-                      maxLength: 253
-                      type: string
-                    weight:
-                      description: |-
-                        Weight is used to determine the proportion of traffic that should be
-                        sent to this model when multiple target models are specified.
+                  Priority defines how important it is to serve the request compared to other requests in the same pool.
+                  Priority is an integer value that defines the priority of the request.
+                  The higher the value, the more critical the request is; negative values _are_ allowed.
+                  No default value is set for this field, allowing for future additions of new fields that may 'one of' with this field.
+                  However, implementations that consume this field (such as the Endpoint Picker) will treat an unset value as '0'.
+                  Priority is used in flow control, primarily in the event of resource scarcity(reqeusts need to be queued).
+                  All requests will be queued, and flow control will _always_ allow requests of higher priority to be served first.
+                  Fairness is only enforced and tracked between requests of the same priority.
 
-                        Weight defines the proportion of requests forwarded to the specified
-                        model. This is computed as weight/(sum of all weights in this
-                        TargetModels list). For non-zero values, there may be some epsilon from
-                        the exact proportion defined here depending on the precision an
-                        implementation supports. Weight is not a percentage and the sum of
-                        weights does not need to equal 100.
-
-                        If a weight is set for any targetModel, it must be set for all targetModels.
-                        Conversely weights are optional, so long as ALL targetModels do not specify a weight.
-                      format: int32
-                      maximum: 1000000
-                      minimum: 1
-                      type: integer
-                  required:
-                  - name
-                  type: object
-                maxItems: 10
-                type: array
-                x-kubernetes-validations:
-                - message: Weights should be set for all models, or none of the models.
-                  rule: self.all(model, has(model.weight)) || self.all(model, !has(model.weight))
+                  Example: requests with Priority 10 will always be served before
+                  requests with Priority of 0 (the value used if Priority is unset or no InfereneceObjective is specified).
+                  Similarly requests with a Priority of -10 will always be served after requests with Priority of 0.
+                type: integer
             required:
             - poolRef
             type: object
diff --git a/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml b/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml
index 138734e..ef73c4a 100644
--- a/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml
+++ b/config/crd/bases/inference.networking.x-k8s.io_inferencepools.yaml
@@ -12,14 +12,15 @@ spec:
     kind: InferencePool
     listKind: InferencePoolList
     plural: inferencepools
+    shortNames:
+    - xinfpool
     singular: inferencepool
   scope: Namespaced
   versions:
   - name: v1alpha2
     schema:
       openAPIV3Schema:
-        description: |
-          InferencePool is the Schema for the InferencePools API.
+        description: InferencePool is the Schema for the InferencePools API.
         properties:
           apiVersion:
             description: |-
diff --git a/config/manifests/gateway/agentgateway/gateway.yaml b/config/manifests/gateway/agentgateway/gateway.yaml
new file mode 100644
index 0000000..9407cc7
--- /dev/null
+++ b/config/manifests/gateway/agentgateway/gateway.yaml
@@ -0,0 +1,10 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: inference-gateway
+spec:
+  gatewayClassName: agentgateway
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
diff --git a/config/manifests/gateway/agentgateway/httproute.yaml b/config/manifests/gateway/agentgateway/httproute.yaml
new file mode 100644
index 0000000..18e90ce
--- /dev/null
+++ b/config/manifests/gateway/agentgateway/httproute.yaml
@@ -0,0 +1,20 @@
+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
+    matches:
+    - path:
+        type: PathPrefix
+        value: /
+    timeouts:
+      request: 300s
diff --git a/config/manifests/inferenceobjective.yaml b/config/manifests/inferenceobjective.yaml
index f4a5de1..3b7ba91 100644
--- a/config/manifests/inferenceobjective.yaml
+++ b/config/manifests/inferenceobjective.yaml
@@ -3,20 +3,19 @@ kind: InferenceObjective
 metadata:
   name: food-review
 spec:
-  criticality: Standard
+  priority: 1
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: food-review-1
-    weight: 100
 ---
 apiVersion: inference.networking.x-k8s.io/v1alpha2
 kind: InferenceObjective
 metadata:
   name: base-model
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
 ---
 apiVersion: inference.networking.x-k8s.io/v1alpha2
@@ -24,6 +23,7 @@ kind: InferenceObjective
 metadata:
   name: base-model-cpu
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
diff --git a/config/manifests/inferencepool-resources.yaml b/config/manifests/inferencepool-resources.yaml
index c8a7fc7..a3eba55 100644
--- a/config/manifests/inferencepool-resources.yaml
+++ b/config/manifests/inferencepool-resources.yaml
@@ -8,9 +8,11 @@ kind: InferencePool
 metadata:
   name: vllm-llama3-8b-instruct
 spec:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   selector:
-    app: vllm-llama3-8b-instruct
+    matchLabels:
+      app: vllm-llama3-8b-instruct
   extensionRef:
     name: vllm-llama3-8b-instruct-epp
 ---
diff --git a/config/manifests/regression-testing/inferenceobjective.yaml b/config/manifests/regression-testing/inferenceobjective.yaml
index 182383c..eede557 100644
--- a/config/manifests/regression-testing/inferenceobjective.yaml
+++ b/config/manifests/regression-testing/inferenceobjective.yaml
@@ -3,12 +3,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-0
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-0
-    weight: 100
 
 ---
 
@@ -17,12 +15,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-1
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-1
-    weight: 100
 
 ---
 
@@ -31,12 +27,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-2
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-2
-    weight: 100
 
 ---
 
@@ -45,12 +39,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-3
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-3
-    weight: 100
 
 ---
 
@@ -59,12 +51,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-4
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-4
-    weight: 100
 
 ---
 
@@ -73,12 +63,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-5
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-5
-    weight: 100
 
 ---
 
@@ -87,12 +75,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-6
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-6
-    weight: 100
 
 ---
 
@@ -101,12 +87,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-7
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-7
-    weight: 100
 
 ---
 
@@ -115,12 +99,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-8
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-8
-    weight: 100
 
 ---
 
@@ -129,12 +111,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-9
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-9
-    weight: 100
 
 ---
 
@@ -143,12 +123,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-10
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-10
-    weight: 100
 
 ---
 
@@ -157,12 +135,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-11
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-11
-    weight: 100
 
 ---
 
@@ -171,12 +147,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-12
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-12
-    weight: 100
 
 
 ---
@@ -186,12 +160,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-13
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-13
-    weight: 100
 
 
 ---
@@ -201,12 +173,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-14
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-14
-    weight: 100
 
 ---
 
@@ -216,6 +186,7 @@ kind: InferenceObjective
 metadata:
   name: base-model
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
-    name: vllm-llama3-8b-instruct
\ No newline at end of file
+    group: inference.networking.k8s.io
+    name: vllm-llama3-8b-instruct
diff --git a/config/observability/prometheus/rbac.yaml b/config/observability/prometheus/rbac.yaml
new file mode 100644
index 0000000..9fad217
--- /dev/null
+++ b/config/observability/prometheus/rbac.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: inference-gateway-sa-metrics-reader
+  namespace: monitoring
diff --git a/config/observability/prometheus/values.yaml b/config/observability/prometheus/values.yaml
new file mode 100644
index 0000000..780b889
--- /dev/null
+++ b/config/observability/prometheus/values.yaml
@@ -0,0 +1,27 @@
+serviceAccounts:
+  server:
+    create: false
+    name: inference-gateway-sa-metrics-reader
+
+extraScrapeConfigs: |
+  - job_name: 'inference-extension-epp'
+    authorization:
+        credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+    scrape_interval: 5s   
+    kubernetes_sd_configs:
+      - role: endpoints
+    relabel_configs:
+      - source_labels: [__meta_kubernetes_service_name]
+        action: keep
+        regex: .*-epp$
+      - source_labels: [__meta_kubernetes_pod_container_port_number]
+        action: keep
+        regex: "9090"
+  - job_name: vllm
+    scrape_interval: 5s   
+    kubernetes_sd_configs:
+      - role: pod
+    relabel_configs:
+      - source_labels: [__meta_kubernetes_pod_label_app]
+        action: keep
+        regex: vllm-llama3-8b-instruct
diff --git a/conformance/reports/v0.5.1/gateway/agentgateway/README.md b/conformance/reports/v0.5.1/gateway/agentgateway/README.md
new file mode 100644
index 0000000..13bb2a2
--- /dev/null
+++ b/conformance/reports/v0.5.1/gateway/agentgateway/README.md
@@ -0,0 +1,11 @@
+# Agentgateway (with kgateway)
+
+## Table of Contents
+
+| Extension Version Tested | Profile Tested | Implementation Version | Mode    | Report                                                                     |
+|--------------------------|----------------|------------------------|---------|----------------------------------------------------------------------------|
+| v0.5.1                   | Gateway        | v0.7.2                 | default | [v0.7.2 report](./inference-v0.7.2-report.yaml)   |
+
+## Reproduce
+
+From the [kgateway repository](https://github.com/kgateway-dev/kgateway/): `CONFORMANCE_GATEWAY_CLASS=agentgateway make gie-conformance`.
diff --git a/conformance/reports/v0.5.1/gateway/agentgateway/inference-v0.7.2-report.yaml b/conformance/reports/v0.5.1/gateway/agentgateway/inference-v0.7.2-report.yaml
new file mode 100644
index 0000000..0f1078e
--- /dev/null
+++ b/conformance/reports/v0.5.1/gateway/agentgateway/inference-v0.7.2-report.yaml
@@ -0,0 +1,23 @@
+GatewayAPIInferenceExtensionVersion: v0.5.1
+apiVersion: gateway.networking.k8s.io/v1
+date: "2025-08-06T17:50:20-07:00"
+gatewayAPIChannel: experimental
+gatewayAPIVersion: v1.3.0
+implementation:
+  contact:
+  - github.com/agentgateway/agentgateway/issues/new/choose
+  organization: agentgateway
+  project: agentgateway
+  url: http://agentgateway.dev/
+  version: v0.7.2
+kind: ConformanceReport
+mode: default
+profiles:
+- core:
+    result: success
+    statistics:
+      Failed: 0
+      Passed: 9
+      Skipped: 0
+  name: Gateway
+  summary: Core tests succeeded.
diff --git a/conformance/reports/v0.5.1/gateway/envoy-ai-gateway/README.md b/conformance/reports/v0.5.1/gateway/envoy-ai-gateway/README.md
new file mode 100644
index 0000000..c95ad3d
--- /dev/null
+++ b/conformance/reports/v0.5.1/gateway/envoy-ai-gateway/README.md
@@ -0,0 +1,33 @@
+# Envoy AI Gateway
+
+## Table of Contents
+
+| Extension Version Tested | Profile Tested | Implementation Version | Mode    | Report                                                                |
+|--------------------------|----------------|------------------------|---------|-----------------------------------------------------------------------|
+| v0.5.1                   | Gateway        | [latest](https://github.com/envoyproxy/ai-gateway)                  | default | [Conformance report](./aigw-latest-report.yaml) |
+| ...                      | ...            | ...                    | ...     | ...                                                                   |
+
+## Reproduce
+
+This is a mirror of the envoy ai gateway [conformance e2e test](https://github.com/envoyproxy/ai-gateway/blob/main/.github/workflows/build_and_test.yaml), which includes the conformance tests for the Gateway API and Inference Extension.
+
+### Prerequisites
+
+In order to run the conformance tests, the following prerequisites must be met:
+
+- The [kubectl](https://kubernetes.io/docs/tasks/tools/) command-line tool installed and configured for the active cluster context.
+- The [helm](https://github.com/helm/helm),[kind](https://kind.sigs.k8s.io), [git](https://git-scm.com/downloads), and [make](https://www.gnu.org/software/make/) command-line tools installed.
+
+### Steps
+
+1. Clone the envoy-ai-gateway repository and checkout the release:
+
+   ```sh
+   git clone https://github.com/envoyproxy/ai-gateway.git && cd ai-gateway
+   ```
+
+2. Running the Gateway API Inference Extension conformance tests:
+
+   ```sh
+      make test-e2e GO_TEST_ARGS="-run TestGatewayAPIInferenceExtension -v" EG_VERSION=v0.0.0-latest TEST_KEEP_CLUSTER=true
+   ```
diff --git a/conformance/reports/v0.5.1/gateway/envoy-ai-gateway/aigw-latest-report.yaml b/conformance/reports/v0.5.1/gateway/envoy-ai-gateway/aigw-latest-report.yaml
new file mode 100644
index 0000000..d445ebe
--- /dev/null
+++ b/conformance/reports/v0.5.1/gateway/envoy-ai-gateway/aigw-latest-report.yaml
@@ -0,0 +1,23 @@
+GatewayAPIInferenceExtensionVersion: v0.5.1
+apiVersion: gateway.networking.k8s.io/v1
+date: "2025-08-15T14:10:31-09:00"
+gatewayAPIChannel: experimental
+gatewayAPIVersion: v1.3.0
+implementation:
+  contact:
+  - github.com/envoyproxy/ai-gateway/issues/new/choose
+  organization: envoyproxy
+  project: envoy-ai-gateway
+  url: github.com/envoyproxy/ai-gateway
+  version: latest
+kind: ConformanceReport
+mode: default
+profiles:
+- core:
+    result: success
+    statistics:
+      Failed: 0
+      Passed: 9
+      Skipped: 0
+  name: Gateway
+  summary: Core tests succeeded.
diff --git a/conformance/resources/base.yaml b/conformance/resources/base.yaml
index fac7ead..5e00a69 100644
--- a/conformance/resources/base.yaml
+++ b/conformance/resources/base.yaml
@@ -153,8 +153,10 @@ metadata:
   namespace: gateway-conformance-app-backend
 spec:
   selector:
-    app: primary-inference-model-server
-  targetPortNumber: 3000
+    matchLabels:
+      app: primary-inference-model-server
+  targetPorts:
+    - number: 3000
   extensionRef:
     name: primary-endpoint-picker-svc
 ---
@@ -246,8 +248,10 @@ metadata:
   namespace: gateway-conformance-app-backend
 spec:
   selector:
-    app: secondary-inference-model-server
-  targetPortNumber: 3000
+    matchLabels:
+      app: primary-inference-model-server
+  targetPorts:
+    - number: 3000
   extensionRef:
     name: secondary-endpoint-picker-svc
     failureMode: FailOpen
diff --git a/conformance/resources/resourcename.go b/conformance/resources/resourcename.go
index 4c0c7b4..6f23840 100644
--- a/conformance/resources/resourcename.go
+++ b/conformance/resources/resourcename.go
@@ -34,6 +34,8 @@ const (
 
 	ModelServerPodReplicas    = 3
 	EndPointPickerPodReplicas = 1
+
+	InferenceObjName = "conformance-fake-model-server"
 )
 
 var (
diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go
index 54d0260..39301e3 100644
--- a/conformance/utils/kubernetes/helpers.go
+++ b/conformance/utils/kubernetes/helpers.go
@@ -105,7 +105,7 @@ func InferencePoolMustHaveCondition(t *testing.T, c client.Reader, poolNN types.
 			}
 
 			for _, parentStatus := range pool.Status.Parents {
-				if parentStatus.GatewayRef.Namespace != nil && string(*parentStatus.GatewayRef.Namespace) == gateway.Namespace && string(parentStatus.GatewayRef.Name) == gateway.Name {
+				if string(parentStatus.GatewayRef.Namespace) == gateway.Namespace && string(parentStatus.GatewayRef.Name) == gateway.Name {
 					if checkCondition(t, parentStatus.Conditions, expectedCondition) {
 						conditionFound = true
 						return true, nil
@@ -131,7 +131,7 @@ func InferencePoolMustHaveCondition(t *testing.T, c client.Reader, poolNN types.
 			}
 			for i, parentStatus := range lastObservedPool.Status.Parents {
 				namespace := parentStatus.GatewayRef.Namespace
-				debugMsg += fmt.Sprintf("\n  Parent %d (Gateway: %s/%s):", i, *namespace, parentStatus.GatewayRef.Name)
+				debugMsg += fmt.Sprintf("\n  Parent %d (Gateway: %s/%s):", i, namespace, parentStatus.GatewayRef.Name)
 				if len(parentStatus.Conditions) == 0 {
 					debugMsg += " (No conditions reported for this parent)"
 				}
diff --git a/docs/proposals/003-model-server-protocol/README.md b/docs/proposals/003-model-server-protocol/README.md
index e6c7cf3..4b27743 100644
--- a/docs/proposals/003-model-server-protocol/README.md
+++ b/docs/proposals/003-model-server-protocol/README.md
@@ -21,10 +21,10 @@ effort.
 The corresponding metrics in vLLM are also shown in the table below, as vLLM is already integrated
 into the reference endpoint picker implementation.
 
-| Metric | Type | Description | vLLM metric | Triton TensorRT-LLM|
-| ----- | ---- | ---- | ---- | ---- |
-| TotalQueuedRequests         | Gauge     | The current total number of requests in the queue.| `vllm:num_requests_waiting`| `nv_trt_llm_request_metrics{request_type=waiting}`|
-| KVCacheUtilization| Gauge     | The current KV cache utilization in percentage.| `vllm:gpu_cache_usage_perc`| `nv_trt_llm_kv_cache_block_metrics{kv_cache_block_type=fraction}`|
+| Metric | Type | Description | vLLM metric | Triton TensorRT-LLM| SGLang |
+| ----- | ---- | ---- | ---- | ---- | ---- |
+| TotalQueuedRequests         | Gauge     | The current total number of requests in the queue.| `vllm:num_requests_waiting`| `nv_trt_llm_request_metrics{request_type=waiting}`| `sglang:num_queue_reqs`
+| KVCacheUtilization| Gauge     | The current KV cache utilization in percentage.| `vllm:gpu_cache_usage_perc`| `nv_trt_llm_kv_cache_block_metrics{kv_cache_block_type=fraction}`| `sglang:token_usage`
 
 
 ### LoRA Adapter Serving
diff --git a/docs/proposals/1199-inferencemodel-api-evolution/README.md b/docs/proposals/1199-inferencemodel-api-evolution/README.md
index e5dedd2..1ad687d 100644
--- a/docs/proposals/1199-inferencemodel-api-evolution/README.md
+++ b/docs/proposals/1199-inferencemodel-api-evolution/README.md
@@ -1,4 +1,4 @@
-# Scheduling Subsystem Architecture
+# InferenceModel API Evolution
 
 Author(s): @kfswain, @ahg-g, @lukeavandrie
 ## Proposal Status
diff --git a/go.mod b/go.mod
index 181e11b..619d8df 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 	go.uber.org/zap v1.27.0
 	golang.org/x/sync v0.16.0
 	google.golang.org/grpc v1.74.2
-	google.golang.org/protobuf v1.36.6
+	google.golang.org/protobuf v1.36.7
 	k8s.io/api v0.33.3
 	k8s.io/apiextensions-apiserver v0.33.3
 	k8s.io/apimachinery v0.33.3
diff --git a/go.sum b/go.sum
index 0a323e0..9ea103f 100644
--- a/go.sum
+++ b/go.sum
@@ -340,8 +340,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
 google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
-google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
-google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
+google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 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=
diff --git a/hack/release-quickstart.sh b/hack/release-quickstart.sh
index 8f25cbd..22c7051 100755
--- a/hack/release-quickstart.sh
+++ b/hack/release-quickstart.sh
@@ -30,12 +30,12 @@ else
 fi
 
 # The vLLM image versions
-# The GPU image is from https://hub.docker.com/layers/vllm/vllm-openai
-VLLM_GPU="${VLLM_GPU:-0.9.1}"
+# The GPU image is from https://hub.docker.com/r/vllm/vllm-openai/tags
+VLLM_GPU="${VLLM_GPU:-0.10.0}"
 # The CPU image is from https://gallery.ecr.aws/q9t5s3a7/vllm-cpu-release-repo
-VLLM_CPU="${VLLM_CPU:-0.9.1}"
+VLLM_CPU="${VLLM_CPU:-0.10.0}"
 # The sim image is from https://github.com/llm-d/llm-d-inference-sim/pkgs/container/llm-d-inference-sim
-VLLM_SIM="${VLLM_SIM:-0.3.0}"
+VLLM_SIM="${VLLM_SIM:-0.3.2-fix}"
 
 echo "Using release tag: ${RELEASE_TAG}"
 echo "Using vLLM GPU image version: ${VLLM_GPU}"
@@ -78,7 +78,7 @@ EPP="config/manifests/inferencepool-resources.yaml"
 #TODO: Put all helm values files into an array to loop over
 EPP_HELM="config/charts/inferencepool/values.yaml"
 BBR_HELM="config/charts/body-based-routing/values.yaml"
-CONFORMANCE_MANIFESTS="conformance/resources/manifests/manifests.yaml"
+CONFORMANCE_MANIFESTS="conformance/resources/base.yaml"
 echo "Updating ${EPP}, ${EPP_HELM}, ${BBR_HELM}, and ${CONFORMANCE_MANIFESTS} ..."
 
 # Update the container tag.
@@ -127,6 +127,15 @@ sed -i.bak -E "s|(llm-d/llm-d-inference-sim:)[^\"[:space:]]+|\1v${VLLM_SIM}|g" "
 # Also change the imagePullPolicy from Always to IfNotPresent on lines containing the vLLM image.
 sed -i.bak '/llm-d\/llm-d-inference-sim/{n;s/Always/IfNotPresent/;}' "$VLLM_SIM_DEPLOY"
 
+# Update the container tag for lora-syncer in vLLM CPU and GPU deployment manifests.
+sed -i.bak -E "s|(us-central1-docker\.pkg\.dev/k8s-staging-images/gateway-api-inference-extension/lora-syncer:)[^\"[:space:]]+|\1${RELEASE_TAG}|g" "$VLLM_GPU_DEPLOY" "$VLLM_CPU_DEPLOY"
+
+# Update the container image pull policy for lora-syncer in vLLM CPU and GPU deployment manifests.
+sed -i.bak '/us-central1-docker.pkg.dev\/k8s-staging-images\/gateway-api-inference-extension\/lora-syncer/{n;s/Always/IfNotPresent/;}' "$VLLM_GPU_DEPLOY" "$VLLM_CPU_DEPLOY"
+
+# Update the container registry for lora-syncer in vLLM CPU and GPU deployment manifests.
+sed -i.bak -E "s|us-central1-docker\.pkg\.dev/k8s-staging-images|registry.k8s.io|g" "$VLLM_GPU_DEPLOY" "$VLLM_CPU_DEPLOY"
+
 # -----------------------------------------------------------------------------
 # Stage the changes
 # -----------------------------------------------------------------------------
diff --git a/pkg/bbr/handlers/request.go b/pkg/bbr/handlers/request.go
index 98cd245..e7baec6 100644
--- a/pkg/bbr/handlers/request.go
+++ b/pkg/bbr/handlers/request.go
@@ -19,7 +19,6 @@ package handlers
 import (
 	"context"
 	"encoding/json"
-	"fmt"
 
 	basepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	eppb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
@@ -31,18 +30,22 @@ import (
 
 const modelHeader = "X-Gateway-Model-Name"
 
+type RequestBody struct {
+	Model string `json:"model"`
+}
+
 // HandleRequestBody handles request bodies.
 func (s *Server) HandleRequestBody(ctx context.Context, requestBodyBytes []byte) ([]*eppb.ProcessingResponse, error) {
 	logger := log.FromContext(ctx)
 	var ret []*eppb.ProcessingResponse
 
-	var requestBody map[string]any
+	var requestBody RequestBody
 	if err := json.Unmarshal(requestBodyBytes, &requestBody); err != nil {
+		metrics.RecordModelNotParsedCounter()
 		return nil, err
 	}
 
-	modelVal, ok := requestBody["model"]
-	if !ok {
+	if requestBody.Model == "" {
 		metrics.RecordModelNotInBodyCounter()
 		logger.V(logutil.DEFAULT).Info("Request body does not contain model parameter")
 		if s.streaming {
@@ -63,13 +66,6 @@ func (s *Server) HandleRequestBody(ctx context.Context, requestBodyBytes []byte)
 		return ret, nil
 	}
 
-	modelStr, ok := modelVal.(string)
-	if !ok {
-		metrics.RecordModelNotParsedCounter()
-		logger.V(logutil.DEFAULT).Info("Model parameter value is not a string")
-		return nil, fmt.Errorf("the model parameter value %v is not a string", modelVal)
-	}
-
 	metrics.RecordSuccessCounter()
 
 	if s.streaming {
@@ -83,7 +79,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, requestBodyBytes []byte)
 								{
 									Header: &basepb.HeaderValue{
 										Key:      modelHeader,
-										RawValue: []byte(modelStr),
+										RawValue: []byte(requestBody.Model),
 									},
 								},
 							},
@@ -108,7 +104,7 @@ func (s *Server) HandleRequestBody(ctx context.Context, requestBodyBytes []byte)
 								{
 									Header: &basepb.HeaderValue{
 										Key:      modelHeader,
-										RawValue: []byte(modelStr),
+										RawValue: []byte(requestBody.Model),
 									},
 								},
 							},
diff --git a/pkg/common/convert.go b/pkg/common/convert.go
deleted file mode 100644
index bb67155..0000000
--- a/pkg/common/convert.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-Copyright 2025 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Package common defines structs for referring to fully qualified k8s resources.
-package common
-
-import (
-	"fmt"
-
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime"
-
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
-	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
-)
-
-func ToUnstructured(obj any) (*unstructured.Unstructured, error) {
-	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
-	if err != nil {
-		return nil, err
-	}
-	return &unstructured.Unstructured{Object: u}, nil
-}
-
-var ToInferencePool = convert[v1.InferencePool]
-
-var ToXInferencePool = convert[v1alpha2.InferencePool]
-
-func convert[T any](u *unstructured.Unstructured) (*T, error) {
-	var res T
-	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &res); err != nil {
-		return nil, fmt.Errorf("error converting unstructured to T: %v", err)
-	}
-	return &res, nil
-}
diff --git a/pkg/epp/backend/metrics/fake.go b/pkg/epp/backend/metrics/fake.go
index 284a264..4e0687f 100644
--- a/pkg/epp/backend/metrics/fake.go
+++ b/pkg/epp/backend/metrics/fake.go
@@ -20,12 +20,14 @@ import (
 	"context"
 	"fmt"
 	"sync"
+	"time"
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
@@ -42,13 +44,23 @@ func (fpm *FakePodMetrics) String() string {
 func (fpm *FakePodMetrics) GetPod() *backend.Pod {
 	return fpm.Pod
 }
+
 func (fpm *FakePodMetrics) GetMetrics() *MetricsState {
 	return fpm.Metrics
 }
+
 func (fpm *FakePodMetrics) UpdatePod(pod *corev1.Pod) {
 	fpm.Pod = toInternalPod(pod)
 }
-func (fpm *FakePodMetrics) StopRefreshLoop() {} // noop
+
+func (*FakePodMetrics) Put(string, datalayer.Cloneable)        {}
+func (*FakePodMetrics) Get(string) (datalayer.Cloneable, bool) { return nil, false }
+func (*FakePodMetrics) Keys() []string                         { return nil }
+
+func (fpm *FakePodMetrics) UpdateMetrics(updated *MetricsState) {
+	updated.UpdateTime = time.Now()
+	fpm.Metrics = updated
+}
 
 type FakePodMetricsClient struct {
 	errMu sync.RWMutex
diff --git a/pkg/epp/backend/metrics/logger.go b/pkg/epp/backend/metrics/logger.go
index bc34546..69fc404 100644
--- a/pkg/epp/backend/metrics/logger.go
+++ b/pkg/epp/backend/metrics/logger.go
@@ -24,7 +24,7 @@ import (
 	"github.com/go-logr/logr"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 
-	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
@@ -33,15 +33,9 @@ const (
 	debugPrintInterval = 5 * time.Second
 )
 
-type Datastore interface {
-	PoolGet() (*v1.InferencePool, error)
-	// PodMetrics operations
-	PodList(func(PodMetrics) bool) []PodMetrics
-}
-
 // StartMetricsLogger starts goroutines to 1) Print metrics debug logs if the DEBUG log level is
 // enabled; 2) flushes Prometheus metrics about the backend servers.
-func StartMetricsLogger(ctx context.Context, datastore Datastore, refreshPrometheusMetricsInterval, metricsStalenessThreshold time.Duration) {
+func StartMetricsLogger(ctx context.Context, datastore datalayer.PoolInfo, refreshPrometheusMetricsInterval, metricsStalenessThreshold time.Duration) {
 	logger := log.FromContext(ctx)
 	ticker := time.NewTicker(refreshPrometheusMetricsInterval)
 	go func() {
@@ -68,9 +62,7 @@ func StartMetricsLogger(ctx context.Context, datastore Datastore, refreshPrometh
 					logger.V(logutil.DEFAULT).Info("Shutting down metrics logger thread")
 					return
 				case <-ticker.C:
-					podsWithFreshMetrics := datastore.PodList(func(pm PodMetrics) bool {
-						return time.Since(pm.GetMetrics().UpdateTime) <= metricsStalenessThreshold
-					})
+					podsWithFreshMetrics := datastore.PodList(PodsWithFreshMetrics(metricsStalenessThreshold))
 					podsWithStaleMetrics := datastore.PodList(func(pm PodMetrics) bool {
 						return time.Since(pm.GetMetrics().UpdateTime) > metricsStalenessThreshold
 					})
@@ -82,7 +74,7 @@ func StartMetricsLogger(ctx context.Context, datastore Datastore, refreshPrometh
 	}
 }
 
-func refreshPrometheusMetrics(logger logr.Logger, datastore Datastore, metricsStalenessThreshold time.Duration) {
+func refreshPrometheusMetrics(logger logr.Logger, datastore datalayer.PoolInfo, metricsStalenessThreshold time.Duration) {
 	pool, err := datastore.PoolGet()
 	if err != nil {
 		// No inference pool or not initialize.
@@ -93,9 +85,7 @@ func refreshPrometheusMetrics(logger logr.Logger, datastore Datastore, metricsSt
 	var kvCacheTotal float64
 	var queueTotal int
 
-	podMetrics := datastore.PodList(func(pm PodMetrics) bool {
-		return time.Since(pm.GetMetrics().UpdateTime) <= metricsStalenessThreshold
-	})
+	podMetrics := datastore.PodList(PodsWithFreshMetrics(metricsStalenessThreshold))
 	logger.V(logutil.TRACE).Info("Refreshing Prometheus Metrics", "ReadyPods", len(podMetrics))
 	if len(podMetrics) == 0 {
 		return
diff --git a/pkg/epp/backend/metrics/metrics_state.go b/pkg/epp/backend/metrics/metrics_state.go
index 0215ac0..50da1cc 100644
--- a/pkg/epp/backend/metrics/metrics_state.go
+++ b/pkg/epp/backend/metrics/metrics_state.go
@@ -17,64 +17,13 @@ limitations under the License.
 package metrics
 
 import (
-	"fmt"
-	"time"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 )
 
 // NewMetricsState initializes a new MetricsState and returns its pointer.
 func NewMetricsState() *MetricsState {
-	return &MetricsState{
-		ActiveModels:  make(map[string]int),
-		WaitingModels: make(map[string]int),
-	}
+	return datalayer.NewMetrics()
 }
 
 // MetricsState holds the latest state of the metrics that were scraped from a pod.
-type MetricsState struct {
-	// ActiveModels is a set of models(including LoRA adapters) that are currently cached to GPU.
-	ActiveModels  map[string]int
-	WaitingModels map[string]int
-	// MaxActiveModels is the maximum number of models that can be loaded to GPU.
-	MaxActiveModels         int
-	RunningQueueSize        int
-	WaitingQueueSize        int
-	KVCacheUsagePercent     float64
-	KvCacheMaxTokenCapacity int
-
-	// UpdateTime record the last time when the metrics were updated.
-	UpdateTime time.Time
-}
-
-// String returns a string with all MetricState information
-func (s *MetricsState) String() string {
-	if s == nil {
-		return ""
-	}
-	return fmt.Sprintf("%+v", *s)
-}
-
-// Clone creates a copy of MetricsState and returns its pointer.
-// Clone returns nil if the object being cloned is nil.
-func (s *MetricsState) Clone() *MetricsState {
-	if s == nil {
-		return nil
-	}
-	activeModels := make(map[string]int, len(s.ActiveModels))
-	for key, value := range s.ActiveModels {
-		activeModels[key] = value
-	}
-	waitingModels := make(map[string]int, len(s.WaitingModels))
-	for key, value := range s.WaitingModels {
-		waitingModels[key] = value
-	}
-	return &MetricsState{
-		ActiveModels:            activeModels,
-		WaitingModels:           waitingModels,
-		MaxActiveModels:         s.MaxActiveModels,
-		RunningQueueSize:        s.RunningQueueSize,
-		WaitingQueueSize:        s.WaitingQueueSize,
-		KVCacheUsagePercent:     s.KVCacheUsagePercent,
-		KvCacheMaxTokenCapacity: s.KvCacheMaxTokenCapacity,
-		UpdateTime:              s.UpdateTime,
-	}
-}
+type MetricsState = datalayer.Metrics
diff --git a/pkg/epp/backend/metrics/pod_metrics.go b/pkg/epp/backend/metrics/pod_metrics.go
index 3471ddf..da66a97 100644
--- a/pkg/epp/backend/metrics/pod_metrics.go
+++ b/pkg/epp/backend/metrics/pod_metrics.go
@@ -28,6 +28,7 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
@@ -39,7 +40,7 @@ type podMetrics struct {
 	pod      atomic.Pointer[backend.Pod]
 	metrics  atomic.Pointer[MetricsState]
 	pmc      PodMetricsClient
-	ds       Datastore
+	ds       datalayer.PoolInfo
 	interval time.Duration
 
 	startOnce sync.Once // ensures the refresh loop goroutine is started only once
@@ -116,7 +117,10 @@ func (pm *podMetrics) refreshMetrics() error {
 	}
 	ctx, cancel := context.WithTimeout(context.Background(), fetchMetricsTimeout)
 	defer cancel()
-	updated, err := pm.pmc.FetchMetrics(ctx, pm.GetPod(), pm.GetMetrics(), pool.Spec.TargetPortNumber)
+	if len(pool.Spec.TargetPorts) != 1 {
+		return fmt.Errorf("expected 1 target port, got %d", len(pool.Spec.TargetPorts))
+	}
+	updated, err := pm.pmc.FetchMetrics(ctx, pm.GetPod(), pm.GetMetrics(), int32(pool.Spec.TargetPorts[0].Number))
 	if err != nil {
 		pm.logger.V(logutil.TRACE).Info("Failed to refreshed metrics:", "err", err)
 	}
@@ -129,17 +133,27 @@ func (pm *podMetrics) refreshMetrics() error {
 	// this case, the updated metrics object will have partial updates. A partial update is
 	// considered better than no updates.
 	if updated != nil {
-		updated.UpdateTime = time.Now()
-		pm.logger.V(logutil.TRACE).Info("Refreshed metrics", "updated", updated)
-		pm.metrics.Store(updated)
+		pm.UpdateMetrics(updated)
 	}
 
 	return nil
 }
 
-func (pm *podMetrics) StopRefreshLoop() {
+func (pm *podMetrics) stopRefreshLoop() {
 	pm.logger.V(logutil.DEFAULT).Info("Stopping refresher", "pod", pm.GetPod())
 	pm.stopOnce.Do(func() {
 		close(pm.done)
 	})
 }
+
+// Allowing forward compatibility between PodMetrics and datalayer.Endpoint, by
+// implementing missing functions (e.g., extended attributes support) as no-op.
+func (*podMetrics) Put(string, datalayer.Cloneable)        {}
+func (*podMetrics) Get(string) (datalayer.Cloneable, bool) { return nil, false }
+func (*podMetrics) Keys() []string                         { return nil }
+
+func (pm *podMetrics) UpdateMetrics(updated *MetricsState) {
+	updated.UpdateTime = time.Now()
+	pm.logger.V(logutil.TRACE).Info("Refreshed metrics", "updated", updated)
+	pm.metrics.Store(updated)
+}
diff --git a/pkg/epp/backend/metrics/types.go b/pkg/epp/backend/metrics/types.go
index 975910f..aadeb85 100644
--- a/pkg/epp/backend/metrics/types.go
+++ b/pkg/epp/backend/metrics/types.go
@@ -25,28 +25,35 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 )
 
 var (
-	AllPodPredicate = func(PodMetrics) bool { return true }
+	AllPodsPredicate = func(PodMetrics) bool { return true }
 )
 
-func NewPodMetricsFactory(pmc PodMetricsClient, refreshMetricsInterval, metricsStalenessThreshold time.Duration) *PodMetricsFactory {
+func PodsWithFreshMetrics(stalenessThreshold time.Duration) func(PodMetrics) bool {
+	return func(pm PodMetrics) bool {
+		if pm == nil {
+			return false // Skip nil pods
+		}
+		return time.Since(pm.GetMetrics().UpdateTime) <= stalenessThreshold
+	}
+}
+
+func NewPodMetricsFactory(pmc PodMetricsClient, refreshMetricsInterval time.Duration) *PodMetricsFactory {
 	return &PodMetricsFactory{
-		pmc:                       pmc,
-		refreshMetricsInterval:    refreshMetricsInterval,
-		metricsStalenessThreshold: metricsStalenessThreshold,
+		pmc:                    pmc,
+		refreshMetricsInterval: refreshMetricsInterval,
 	}
 }
 
 type PodMetricsFactory struct {
-	pmc                       PodMetricsClient
-	refreshMetricsInterval    time.Duration
-	metricsStalenessThreshold time.Duration
+	pmc                    PodMetricsClient
+	refreshMetricsInterval time.Duration
 }
 
-func (f *PodMetricsFactory) NewPodMetrics(parentCtx context.Context, in *corev1.Pod, ds Datastore) PodMetrics {
+func (f *PodMetricsFactory) NewEndpoint(parentCtx context.Context, in *corev1.Pod, ds datalayer.PoolInfo) PodMetrics {
 	pod := toInternalPod(in)
 	pm := &podMetrics{
 		pmc:       f.pmc,
@@ -64,10 +71,10 @@ func (f *PodMetricsFactory) NewPodMetrics(parentCtx context.Context, in *corev1.
 	return pm
 }
 
-type PodMetrics interface {
-	GetPod() *backend.Pod
-	GetMetrics() *MetricsState
-	UpdatePod(*corev1.Pod)
-	StopRefreshLoop()
-	String() string
+func (f *PodMetricsFactory) ReleaseEndpoint(ep PodMetrics) {
+	if pm, ok := ep.(*podMetrics); ok {
+		pm.stopRefreshLoop()
+	}
 }
+
+type PodMetrics = datalayer.Endpoint
diff --git a/pkg/epp/backend/pod.go b/pkg/epp/backend/pod.go
index 3340a3d..324a747 100644
--- a/pkg/epp/backend/pod.go
+++ b/pkg/epp/backend/pod.go
@@ -17,38 +17,7 @@ limitations under the License.
 package backend
 
 import (
-	"fmt"
-
-	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 )
 
-type Pod struct {
-	NamespacedName types.NamespacedName
-	Address        string
-	Labels         map[string]string
-}
-
-func (p *Pod) String() string {
-	if p == nil {
-		return ""
-	}
-	return fmt.Sprintf("%+v", *p)
-}
-
-func (p *Pod) Clone() *Pod {
-	if p == nil {
-		return nil
-	}
-	clonedLabels := make(map[string]string, len(p.Labels))
-	for key, value := range p.Labels {
-		clonedLabels[key] = value
-	}
-	return &Pod{
-		NamespacedName: types.NamespacedName{
-			Name:      p.NamespacedName.Name,
-			Namespace: p.NamespacedName.Namespace,
-		},
-		Address: p.Address,
-		Labels:  clonedLabels,
-	}
-}
+type Pod = datalayer.PodInfo
diff --git a/pkg/epp/controller/inferenceobjective_reconciler.go b/pkg/epp/controller/inferenceobjective_reconciler.go
index d96298e..53bce86 100644
--- a/pkg/epp/controller/inferenceobjective_reconciler.go
+++ b/pkg/epp/controller/inferenceobjective_reconciler.go
@@ -39,7 +39,7 @@ type InferenceObjectiveReconciler struct {
 }
 
 func (c *InferenceObjectiveReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	logger := log.FromContext(ctx).V(logutil.DEFAULT).WithValues("InferenceObjective", req.NamespacedName)
+	logger := log.FromContext(ctx).V(logutil.DEFAULT)
 	ctx = ctrl.LoggerInto(ctx, logger)
 
 	logger.Info("Reconciling InferenceObjective")
@@ -54,7 +54,7 @@ func (c *InferenceObjectiveReconciler) Reconcile(ctx context.Context, req ctrl.R
 		notFound = true
 	}
 
-	if notFound || !infObjective.DeletionTimestamp.IsZero() || infObjective.Spec.PoolRef.Name != v1alpha2.ObjectName(c.PoolGKNN.Name) {
+	if notFound || !infObjective.DeletionTimestamp.IsZero() || infObjective.Spec.PoolRef.Name != v1alpha2.ObjectName(c.PoolGKNN.Name) || infObjective.Spec.PoolRef.Group != v1alpha2.Group(c.PoolGKNN.Group) {
 		// InferenceObjective object got deleted or changed the referenced pool.
 		c.Datastore.ObjectiveDelete(req.NamespacedName)
 		return ctrl.Result{}, nil
@@ -83,5 +83,5 @@ func (c *InferenceObjectiveReconciler) SetupWithManager(ctx context.Context, mgr
 }
 
 func (c *InferenceObjectiveReconciler) eventPredicate(infObjective *v1alpha2.InferenceObjective) bool {
-	return string(infObjective.Spec.PoolRef.Name) == c.PoolGKNN.Name
+	return string(infObjective.Spec.PoolRef.Name) == c.PoolGKNN.Name && string(infObjective.Spec.PoolRef.Group) == c.PoolGKNN.Group
 }
diff --git a/pkg/epp/controller/inferencepool_reconciler.go b/pkg/epp/controller/inferencepool_reconciler.go
index 1d3c0aa..d8b7668 100644
--- a/pkg/epp/controller/inferencepool_reconciler.go
+++ b/pkg/epp/controller/inferencepool_reconciler.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 
 	"k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/log"
@@ -43,10 +42,10 @@ type InferencePoolReconciler struct {
 }
 
 func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	logger := log.FromContext(ctx).WithValues("group", c.PoolGKNN.Group, "inferencePool", req.NamespacedName).V(logutil.DEFAULT)
+	logger := log.FromContext(ctx).WithValues("group", c.PoolGKNN.Group).V(logutil.DEFAULT)
 	ctx = ctrl.LoggerInto(ctx, logger)
 
-	logger.Info("Reconciling InferencePool", "group", c.PoolGKNN.Group, "inferencePool", req.NamespacedName)
+	logger.Info("Reconciling InferencePool")
 
 	// 1. Initialize a generic client.Object based on the group.
 	var obj client.Object
@@ -81,23 +80,17 @@ func (c *InferencePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 	}
 
 	// 4. Convert the fetched object to the canonical v1.InferencePool.
-	var v1infPool *v1.InferencePool
+	v1infPool := &v1.InferencePool{}
 
 	switch pool := obj.(type) {
 	case *v1.InferencePool:
 		// If it's already a v1 object, just use it.
 		v1infPool = pool
 	case *v1alpha2.InferencePool:
-		// If it's a v1alpha2 object, convert it to v1.
-		var uns *unstructured.Unstructured
-		uns, err := common.ToUnstructured(pool)
+		var err error
+		err = pool.ConvertTo(v1infPool)
 		if err != nil {
-			logger.Error(err, "Failed to convert inferencePool to unstructured")
-			return ctrl.Result{}, err
-		}
-		v1infPool, err = common.ToInferencePool(uns)
-		if err != nil {
-			logger.Error(err, "Failed to convert unstructured to inferencePool")
+			logger.Error(err, "Failed to convert XInferencePool to InferencePool")
 			return ctrl.Result{}, err
 		}
 	default:
diff --git a/pkg/epp/controller/pod_reconciler.go b/pkg/epp/controller/pod_reconciler.go
index 99f34c1..3cd7c25 100644
--- a/pkg/epp/controller/pod_reconciler.go
+++ b/pkg/epp/controller/pod_reconciler.go
@@ -47,7 +47,7 @@ func (c *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
 		return ctrl.Result{}, nil
 	}
 
-	logger.V(logutil.VERBOSE).Info("Pod being reconciled", "name", req.NamespacedName)
+	logger.V(logutil.VERBOSE).Info("Pod being reconciled")
 
 	pod := &corev1.Pod{}
 	if err := c.Get(ctx, req.NamespacedName, pod); err != nil {
@@ -55,7 +55,7 @@ func (c *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
 			c.Datastore.PodDelete(req.NamespacedName)
 			return ctrl.Result{}, nil
 		}
-		logger.V(logutil.DEFAULT).Error(err, "Unable to get pod", "name", req.NamespacedName)
+		logger.V(logutil.DEFAULT).Error(err, "Unable to get pod")
 		return ctrl.Result{}, err
 	}
 
@@ -92,13 +92,13 @@ func (c *PodReconciler) SetupWithManager(mgr ctrl.Manager) error {
 func (c *PodReconciler) updateDatastore(logger logr.Logger, pod *corev1.Pod) {
 	namespacedName := types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}
 	if !podutil.IsPodReady(pod) || !c.Datastore.PoolLabelsMatch(pod.Labels) {
-		logger.V(logutil.DEBUG).Info("Pod removed or not added", "name", namespacedName)
+		logger.V(logutil.DEBUG).Info("Pod removed or not added")
 		c.Datastore.PodDelete(namespacedName)
 	} else {
 		if c.Datastore.PodUpdateOrAddIfNotExist(pod) {
-			logger.V(logutil.DEFAULT).Info("Pod added", "name", namespacedName)
+			logger.V(logutil.DEFAULT).Info("Pod added")
 		} else {
-			logger.V(logutil.DEFAULT).Info("Pod already exists", "name", namespacedName)
+			logger.V(logutil.DEFAULT).Info("Pod already exists")
 		}
 	}
 }
diff --git a/pkg/epp/datalayer/collector.go b/pkg/epp/datalayer/collector.go
index 94aa9b6..87d2a62 100644
--- a/pkg/epp/datalayer/collector.go
+++ b/pkg/epp/datalayer/collector.go
@@ -83,10 +83,13 @@ func NewCollector() *Collector {
 
 // Start initiates data source collection for the endpoint.
 func (c *Collector) Start(ctx context.Context, ticker Ticker, ep Endpoint, sources []DataSource) error {
+	var ready chan struct{}
 	started := false
+
 	c.startOnce.Do(func() {
 		c.ctx, c.cancel = context.WithCancel(ctx)
 		started = true
+		ready = make(chan struct{})
 
 		go func(endpoint Endpoint, sources []DataSource) {
 			logger := log.FromContext(ctx).WithValues("endpoint", ep.GetPod().GetIPAddress())
@@ -97,6 +100,8 @@ func (c *Collector) Start(ctx context.Context, ticker Ticker, ep Endpoint, sourc
 				ticker.Stop()
 			}()
 
+			close(ready) // signal ready to accept ticks
+
 			for {
 				select {
 				case <-c.ctx.Done(): // per endpoint context cancelled
@@ -115,7 +120,21 @@ func (c *Collector) Start(ctx context.Context, ticker Ticker, ep Endpoint, sourc
 	if !started {
 		return errors.New("collector start called multiple times")
 	}
-	return nil
+
+	// Wait for goroutine to signal readiness.
+	// The use of ready channel is mostly to make the function testable, by ensuring
+	// synchronous order of events. Ignoring test requirements, one could let the
+	// go routine start at some arbitrary point in the future, possibly after this
+	// function has returned.
+	select {
+	case <-ready:
+		return nil
+	case <-ctx.Done():
+		if c.cancel != nil {
+			c.cancel() // ensure clean up
+		}
+		return ctx.Err()
+	}
 }
 
 // Stop terminates the collector.
diff --git a/pkg/epp/datalayer/endpoint.go b/pkg/epp/datalayer/endpoint.go
index 31f24c2..2a72886 100644
--- a/pkg/epp/datalayer/endpoint.go
+++ b/pkg/epp/datalayer/endpoint.go
@@ -37,6 +37,7 @@ type EndpointMetricsState interface {
 
 // Endpoint represents an inference serving endpoint and its related attributes.
 type Endpoint interface {
+	fmt.Stringer
 	EndpointPodState
 	EndpointMetricsState
 	AttributeMap
@@ -57,7 +58,7 @@ func NewEndpoint() *ModelServer {
 }
 
 // String returns a representation of the ModelServer. For brevity, only names of
-// extended attributes are returned and not the values.
+// extended attributes are returned and not their values.
 func (srv *ModelServer) String() string {
 	return fmt.Sprintf("Pod: %v; Metrics: %v; Attributes: %v", srv.GetPod(), srv.GetMetrics(), srv.Keys())
 }
diff --git a/pkg/epp/datalayer/factory.go b/pkg/epp/datalayer/factory.go
new file mode 100644
index 0000000..a282069
--- /dev/null
+++ b/pkg/epp/datalayer/factory.go
@@ -0,0 +1,46 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package datalayer
+
+import (
+	"context"
+
+	corev1 "k8s.io/api/core/v1"
+
+	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+// PoolInfo represents the DataStore information needed for endpoints.
+// TODO:
+// Consider if to remove/simplify in follow-ups. This is mostly for backward
+// compatibility with backend.metrics' expectations and allowing a shared
+// implementation during the transition.
+//   - Endpoint metric scraping uses PoolGet to access the pool's Port and Name.
+//   - Global metrics logging uses PoolGet solely for error return and PodList to enumerate
+//     all endpoints for metrics summarization.
+type PoolInfo interface {
+	PoolGet() (*v1.InferencePool, error)
+	PodList(func(Endpoint) bool) []Endpoint
+}
+
+// EndpointFactory defines an interface for managing Endpoint lifecycle. Specifically,
+// providing methods to allocate and retire endpoints. This can potentially be used for
+// pooled memory or other management chores in the implementation.
+type EndpointFactory interface {
+	NewEndpoint(parent context.Context, inpod *corev1.Pod, poolinfo PoolInfo) Endpoint
+	ReleaseEndpoint(ep Endpoint)
+}
diff --git a/pkg/epp/datastore/datastore.go b/pkg/epp/datastore/datastore.go
index b1dee7f..00275b9 100644
--- a/pkg/epp/datastore/datastore.go
+++ b/pkg/epp/datastore/datastore.go
@@ -32,6 +32,7 @@ import (
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 	podutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/pod"
 )
@@ -66,13 +67,13 @@ type Datastore interface {
 	Clear()
 }
 
-func NewDatastore(parentCtx context.Context, pmf *backendmetrics.PodMetricsFactory) Datastore {
+func NewDatastore(parentCtx context.Context, epFactory datalayer.EndpointFactory) Datastore {
 	store := &datastore{
 		parentCtx:           parentCtx,
 		poolAndObjectivesMu: sync.RWMutex{},
 		objectives:          make(map[string]*v1alpha2.InferenceObjective),
 		pods:                &sync.Map{},
-		pmf:                 pmf,
+		epf:                 epFactory,
 	}
 	return store
 }
@@ -87,7 +88,7 @@ type datastore struct {
 	objectives map[string]*v1alpha2.InferenceObjective
 	// key: types.NamespacedName, value: backendmetrics.PodMetrics
 	pods *sync.Map
-	pmf  *backendmetrics.PodMetricsFactory
+	epf  datalayer.EndpointFactory
 }
 
 func (ds *datastore) Clear() {
@@ -97,7 +98,7 @@ func (ds *datastore) Clear() {
 	ds.objectives = make(map[string]*v1alpha2.InferenceObjective)
 	// stop all pods go routines before clearing the pods map.
 	ds.pods.Range(func(_, v any) bool {
-		v.(backendmetrics.PodMetrics).StopRefreshLoop()
+		ds.epf.ReleaseEndpoint(v.(backendmetrics.PodMetrics))
 		return true
 	})
 	ds.pods.Clear()
@@ -152,7 +153,7 @@ func (ds *datastore) PoolLabelsMatch(podLabels map[string]string) bool {
 	if ds.pool == nil {
 		return false
 	}
-	poolSelector := selectorFromInferencePoolSelector(ds.pool.Spec.Selector)
+	poolSelector := selectorFromInferencePoolSelector(ds.pool.Spec.Selector.MatchLabels)
 	podSet := labels.Set(podLabels)
 	return poolSelector.Matches(podSet)
 }
@@ -215,7 +216,7 @@ func (ds *datastore) PodUpdateOrAddIfNotExist(pod *corev1.Pod) bool {
 	var pm backendmetrics.PodMetrics
 	existing, ok := ds.pods.Load(namespacedName)
 	if !ok {
-		pm = ds.pmf.NewPodMetrics(ds.parentCtx, pod, ds)
+		pm = ds.epf.NewEndpoint(ds.parentCtx, pod, ds)
 		ds.pods.Store(namespacedName, pm)
 	} else {
 		pm = existing.(backendmetrics.PodMetrics)
@@ -228,8 +229,7 @@ func (ds *datastore) PodUpdateOrAddIfNotExist(pod *corev1.Pod) bool {
 func (ds *datastore) PodDelete(namespacedName types.NamespacedName) {
 	v, ok := ds.pods.LoadAndDelete(namespacedName)
 	if ok {
-		pmr := v.(backendmetrics.PodMetrics)
-		pmr.StopRefreshLoop()
+		ds.epf.ReleaseEndpoint(v.(backendmetrics.PodMetrics))
 	}
 }
 
@@ -237,7 +237,7 @@ func (ds *datastore) podResyncAll(ctx context.Context, reader client.Reader) err
 	logger := log.FromContext(ctx)
 	podList := &corev1.PodList{}
 	if err := reader.List(ctx, podList, &client.ListOptions{
-		LabelSelector: selectorFromInferencePoolSelector(ds.pool.Spec.Selector),
+		LabelSelector: selectorFromInferencePoolSelector(ds.pool.Spec.Selector.MatchLabels),
 		Namespace:     ds.pool.Namespace,
 	}); err != nil {
 		return fmt.Errorf("failed to list pods - %w", err)
diff --git a/pkg/epp/flowcontrol/contracts/mocks/mocks.go b/pkg/epp/flowcontrol/contracts/mocks/mocks.go
index 5431e2c..c5c8d2e 100644
--- a/pkg/epp/flowcontrol/contracts/mocks/mocks.go
+++ b/pkg/epp/flowcontrol/contracts/mocks/mocks.go
@@ -46,9 +46,8 @@ import (
 type MockRegistryShard struct {
 	IDFunc                       func() string
 	IsActiveFunc                 func() bool
-	ActiveManagedQueueFunc       func(flowID string) (contracts.ManagedQueue, error)
-	ManagedQueueFunc             func(flowID string, priority uint) (contracts.ManagedQueue, error)
-	IntraFlowDispatchPolicyFunc  func(flowID string, priority uint) (framework.IntraFlowDispatchPolicy, error)
+	ManagedQueueFunc             func(key types.FlowKey) (contracts.ManagedQueue, error)
+	IntraFlowDispatchPolicyFunc  func(key types.FlowKey) (framework.IntraFlowDispatchPolicy, error)
 	InterFlowDispatchPolicyFunc  func(priority uint) (framework.InterFlowDispatchPolicy, error)
 	PriorityBandAccessorFunc     func(priority uint) (framework.PriorityBandAccessor, error)
 	AllOrderedPriorityLevelsFunc func() []uint
@@ -69,23 +68,16 @@ func (m *MockRegistryShard) IsActive() bool {
 	return false
 }
 
-func (m *MockRegistryShard) ActiveManagedQueue(flowID string) (contracts.ManagedQueue, error) {
-	if m.ActiveManagedQueueFunc != nil {
-		return m.ActiveManagedQueueFunc(flowID)
-	}
-	return nil, nil
-}
-
-func (m *MockRegistryShard) ManagedQueue(flowID string, priority uint) (contracts.ManagedQueue, error) {
+func (m *MockRegistryShard) ManagedQueue(key types.FlowKey) (contracts.ManagedQueue, error) {
 	if m.ManagedQueueFunc != nil {
-		return m.ManagedQueueFunc(flowID, priority)
+		return m.ManagedQueueFunc(key)
 	}
 	return nil, nil
 }
 
-func (m *MockRegistryShard) IntraFlowDispatchPolicy(flowID string, priority uint) (framework.IntraFlowDispatchPolicy, error) {
+func (m *MockRegistryShard) IntraFlowDispatchPolicy(key types.FlowKey) (framework.IntraFlowDispatchPolicy, error) {
 	if m.IntraFlowDispatchPolicyFunc != nil {
-		return m.IntraFlowDispatchPolicyFunc(flowID, priority)
+		return m.IntraFlowDispatchPolicyFunc(key)
 	}
 	return nil, nil
 }
@@ -148,8 +140,8 @@ func (m *MockSaturationDetector) IsSaturated(ctx context.Context) bool {
 //  3. **Self-Wiring**: The `FlowQueueAccessor()` method returns the mock itself, ensuring the accessor is always
 //     correctly connected to the queue's state without manual wiring in tests.
 type MockManagedQueue struct {
-	// FlowSpecV defines the flow specification for this mock queue. It should be set by the test.
-	FlowSpecV types.FlowSpecification
+	// FlowKeyV defines the flow specification for this mock queue. It should be set by the test.
+	FlowKeyV types.FlowKey
 
 	// AddFunc allows a test to completely override the default Add behavior.
 	AddFunc func(item types.QueueItemAccessor) error
@@ -249,7 +241,7 @@ func (m *MockManagedQueue) Drain() ([]types.QueueItemAccessor, error) {
 	return drained, nil
 }
 
-func (m *MockManagedQueue) FlowSpec() types.FlowSpecification         { return m.FlowSpecV }
+func (m *MockManagedQueue) FlowKey() types.FlowKey                    { return m.FlowKeyV }
 func (m *MockManagedQueue) Name() string                              { return "" }
 func (m *MockManagedQueue) Capabilities() []framework.QueueCapability { return nil }
 func (m *MockManagedQueue) Comparator() framework.ItemComparator      { return nil }
diff --git a/pkg/epp/flowcontrol/contracts/registry.go b/pkg/epp/flowcontrol/contracts/registry.go
index 843f501..b823ae0 100644
--- a/pkg/epp/flowcontrol/contracts/registry.go
+++ b/pkg/epp/flowcontrol/contracts/registry.go
@@ -24,11 +24,13 @@ package contracts
 
 import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
 )
 
 // RegistryShard defines the read-oriented interface that a `controller.FlowController` worker uses to access its
-// specific slice (shard) of the `FlowRegistry`'s state. It provides the necessary methods for a worker to perform its
-// dispatch operations by accessing queues and policies in a concurrent-safe manner.
+// specific slice (shard) of the `FlowRegistry's` state. It provides a concurrent-safe view of all flow instances, which
+// are uniquely identified by their composite `types.FlowKey`. It is the primary contract for performing dispatch
+// operations.
 //
 // # Conformance
 //
@@ -41,21 +43,19 @@ type RegistryShard interface {
 	// being gracefully drained and should not be given new work.
 	IsActive() bool
 
-	// ActiveManagedQueue returns the currently active `ManagedQueue` for a given flow on this shard. This is the queue to
-	// which new requests for the flow should be enqueued.
-	// Returns an error wrapping `ErrFlowInstanceNotFound` if no active instance exists for the given `flowID`.
-	ActiveManagedQueue(flowID string) (ManagedQueue, error)
-
-	// ManagedQueue retrieves a specific (potentially draining) `ManagedQueue` instance from this shard. This allows a
-	// worker to continue dispatching items from queues that are draining as part of a flow update.
-	// Returns an error wrapping `ErrFlowInstanceNotFound` if no instance for the given flowID and priority exists.
-	ManagedQueue(flowID string, priority uint) (ManagedQueue, error)
+	// ManagedQueue retrieves the managed queue for the given, unique `types.FlowKey`. This is the primary method for
+	// accessing a specific flow's queue for either enqueueing or dispatching requests.
+	//
+	// Returns an error wrapping `ErrPriorityBandNotFound` if the priority specified in the key is not configured, or
+	// `ErrFlowInstanceNotFound` if no instance exists for the given `key`.
+	ManagedQueue(key types.FlowKey) (ManagedQueue, error)
 
-	// IntraFlowDispatchPolicy retrieves a flow's configured `framework.IntraFlowDispatchPolicy` for this shard.
+	// IntraFlowDispatchPolicy retrieves a flow's configured `framework.IntraFlowDispatchPolicy` for this shard,
+	// identified by its unique `FlowKey`.
 	// The registry guarantees that a non-nil default policy (as configured at the priority-band level) is returned if
-	// none is specified on the flow itself.
+	// none is specified for the flow.
 	// Returns an error wrapping `ErrFlowInstanceNotFound` if the flow instance does not exist.
-	IntraFlowDispatchPolicy(flowID string, priority uint) (framework.IntraFlowDispatchPolicy, error)
+	IntraFlowDispatchPolicy(key types.FlowKey) (framework.IntraFlowDispatchPolicy, error)
 
 	// InterFlowDispatchPolicy retrieves a priority band's configured `framework.InterFlowDispatchPolicy` for this shard.
 	// The registry guarantees that a non-nil default policy is returned if none is configured for the band.
@@ -86,8 +86,6 @@ type RegistryShard interface {
 // # Conformance
 //
 //   - All methods (including those embedded from `framework.SafeQueue`) MUST be goroutine-safe.
-//   - The `Add()` method MUST reject new items if the queue has been marked as "draining" by the `FlowRegistry`,
-//     ensuring that lifecycle changes are respected even by consumers holding a stale pointer to the queue.
 //   - All mutating methods (`Add()`, `Remove()`, `Cleanup()`, `Drain()`) MUST atomically update relevant statistics
 //     (e.g., queue length, byte size).
 type ManagedQueue interface {
diff --git a/pkg/epp/flowcontrol/controller/internal/filter.go b/pkg/epp/flowcontrol/controller/internal/filter.go
index f7e7313..0a06692 100644
--- a/pkg/epp/flowcontrol/controller/internal/filter.go
+++ b/pkg/epp/flowcontrol/controller/internal/filter.go
@@ -23,12 +23,13 @@ import (
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/contracts"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
 // BandFilter is a function that acts as a pre-policy gate. It takes a complete view of a priority band and returns a
-// subset of flow IDs that are considered viable candidates for a subsequent policy decision. It can also return a
-// boolean signal to pause the entire operation for the band.
+// potentially filtered `framework.PriorityBandAccessor` containing only the flows that are viable candidates for a
+// subsequent policy decision. It can also return a boolean signal to pause the entire operation for the band.
 //
 // This abstraction decouples the logic of determining *viability* (e.g., is a flow subject to backpressure?) from the
 // logic of *selection* (e.g., which of the viable flows is the fairest to pick next?). This separation simplifies the
@@ -39,13 +40,13 @@ import (
 // can be chained to apply different viability criteria. For example, a future filter could be developed to temporarily
 // exclude a "misbehaving" flow that is causing repeated errors, quarantining it from policy consideration.
 //
-// A nil `allowedFlows` map indicates that no filtering is necessary and all flows in the band are visible.
-// This provides a zero-allocation fast path for the common case where no flows are being filtered.
+// A nil returned `PriorityBandAccessor` indicates that no filtering was necessary and the original accessor should be
+// used. This provides a zero-allocation fast path for the common case where no flows are being filtered.
 type BandFilter func(
 	ctx context.Context,
 	band framework.PriorityBandAccessor,
 	logger logr.Logger,
-) (allowedFlows map[string]struct{}, shouldPause bool)
+) (filteredBand framework.PriorityBandAccessor, shouldPause bool)
 
 // NewSaturationFilter creates a `BandFilter` that uses the provided `contracts.SaturationDetector` to determine which
 // flows are dispatchable. This is the standard filter used in the production `FlowController` for the dispatch
@@ -55,7 +56,7 @@ func NewSaturationFilter(sd contracts.SaturationDetector) BandFilter {
 		ctx context.Context,
 		band framework.PriorityBandAccessor,
 		logger logr.Logger,
-	) (map[string]struct{}, bool) {
+	) (framework.PriorityBandAccessor, bool) {
 		// Phase 1: Implement the current global saturation check.
 		if sd.IsSaturated(ctx) {
 			logger.V(logutil.VERBOSE).Info("System saturated, pausing dispatch for this shard.")
@@ -63,7 +64,8 @@ func NewSaturationFilter(sd contracts.SaturationDetector) BandFilter {
 		}
 
 		// Phase 2 (Future): This is where per-flow saturation logic would go.
-		// It would iterate `band`, call `IsSaturated(ctx, flowID)`, and build a filtered map of allowed flows.
+		// It would iterate `band`, call `IsSaturated(ctx, flowID)`, and build a filtered map of allowed flows,
+		// then return `newSubsetPriorityBandAccessor(band, allowedFlows)`.
 		// For now, no per-flow filtering is done. Return nil to signal the fast path.
 		return nil, false // Do not pause, and do not filter any flows.
 	}
@@ -73,31 +75,28 @@ func NewSaturationFilter(sd contracts.SaturationDetector) BandFilter {
 // It implements the `framework.PriorityBandAccessor` interface, ensuring that any policy operating on it will only
 // see the allowed flows, regardless of which accessor method is used. This provides correctness by construction.
 //
-// For performance, it pre-computes a slice of the allowed flow IDs at creation time, making subsequent calls to
-// `FlowIDs()` an O(1) operation with zero allocations.
+// For performance, it pre-computes a slice of the allowed flows at creation time, making subsequent calls to
+// `FlowKeys()` an O(1) operation with zero allocations.
 type subsetPriorityBandAccessor struct {
 	originalAccessor  framework.PriorityBandAccessor
-	allowedFlows      map[string]struct{}
-	allowedFlowsSlice []string
+	allowedFlows      map[types.FlowKey]struct{}
+	allowedFlowsSlice []types.FlowKey
 }
 
 var _ framework.PriorityBandAccessor = &subsetPriorityBandAccessor{}
 
 // newSubsetPriorityBandAccessor creates a new filtered view of a priority band.
-func newSubsetPriorityBandAccessor(
-	original framework.PriorityBandAccessor,
-	allowed map[string]struct{},
-) *subsetPriorityBandAccessor {
-	// Pre-compute the slice of flow IDs for performance.
-	ids := make([]string, 0, len(allowed))
-	for id := range allowed {
-		ids = append(ids, id)
+func newSubsetPriorityBandAccessor(original framework.PriorityBandAccessor, allowed []types.FlowKey) *subsetPriorityBandAccessor {
+	// Pre-compute the map for efficient lookups in `Queue()` and `IterateQueues()`.
+	allowedMap := make(map[types.FlowKey]struct{}, len(allowed))
+	for _, k := range allowed {
+		allowedMap[k] = struct{}{}
 	}
 
 	return &subsetPriorityBandAccessor{
 		originalAccessor:  original,
-		allowedFlows:      allowed,
-		allowedFlowsSlice: ids,
+		allowedFlows:      allowedMap,
+		allowedFlowsSlice: allowed,
 	}
 }
 
@@ -111,19 +110,21 @@ func (s *subsetPriorityBandAccessor) PriorityName() string {
 	return s.originalAccessor.PriorityName()
 }
 
-// FlowIDs returns a slice of all flow IDs within this priority band that are in the allowed subset.
+// FlowKeys returns a slice of the composite `types.FlowKey`s for every flow instance currently active within this
+// priority band that are in the allowed subset.
 // This is an O(1) operation because the slice is pre-computed at creation.
-func (s *subsetPriorityBandAccessor) FlowIDs() []string {
+func (s *subsetPriorityBandAccessor) FlowKeys() []types.FlowKey {
 	return s.allowedFlowsSlice
 }
 
-// Queue returns a `framework.FlowQueueAccessor` for the specified `flowID` within this priority band, but only if it is
+// Queue returns a `framework.FlowQueueAccessor` for the specified `ID` within this priority band, but only if it is
 // in the allowed subset. This is an O(1) map lookup. If the flow is not in the allowed subset, it returns nil.
-func (s *subsetPriorityBandAccessor) Queue(flowID string) framework.FlowQueueAccessor {
-	if _, ok := s.allowedFlows[flowID]; !ok {
+func (s *subsetPriorityBandAccessor) Queue(id string) framework.FlowQueueAccessor {
+	key := types.FlowKey{ID: id, Priority: s.Priority()}
+	if _, ok := s.allowedFlows[key]; !ok {
 		return nil
 	}
-	return s.originalAccessor.Queue(flowID)
+	return s.originalAccessor.Queue(id)
 }
 
 // IterateQueues executes the given `callback` for each `framework.FlowQueueAccessor` in the allowed subset of this
@@ -132,7 +133,7 @@ func (s *subsetPriorityBandAccessor) Queue(flowID string) framework.FlowQueueAcc
 // efficient than iterating over a pre-computed slice of IDs.
 func (s *subsetPriorityBandAccessor) IterateQueues(callback func(queue framework.FlowQueueAccessor) bool) {
 	s.originalAccessor.IterateQueues(func(queue framework.FlowQueueAccessor) bool {
-		if _, ok := s.allowedFlows[queue.FlowSpec().ID]; ok {
+		if _, ok := s.allowedFlows[queue.FlowKey()]; ok {
 			// This queue is in the allowed set, so execute the callback.
 			if !callback(queue) {
 				return false // The callback requested to stop, so we stop the outer iteration too.
diff --git a/pkg/epp/flowcontrol/controller/internal/processor.go b/pkg/epp/flowcontrol/controller/internal/processor.go
index cef5b35..7f9c8ee 100644
--- a/pkg/epp/flowcontrol/controller/internal/processor.go
+++ b/pkg/epp/flowcontrol/controller/internal/processor.go
@@ -199,36 +199,37 @@ func (sp *ShardProcessor) Enqueue(item *flowItem) {
 // main `Run` goroutine, making its "check-then-act" logic for capacity safe.
 func (sp *ShardProcessor) enqueue(item *flowItem) {
 	req := item.OriginalRequest()
+	key := req.FlowKey()
+
 	logger := log.FromContext(req.Context()).WithName("enqueue").WithValues(
-		"flowID", req.FlowID(),
+		"flowKey", key,
+		"flowID", key.ID,
+		"priority", key.Priority,
 		"reqID", req.ID(),
 		"reqByteSize", req.ByteSize(),
 	)
 
-	managedQ, err := sp.shard.ActiveManagedQueue(req.FlowID())
+	managedQ, err := sp.shard.ManagedQueue(key)
 	if err != nil {
-		// This is a significant configuration error; an active queue should exist for a valid flow.
-		finalErr := fmt.Errorf("configuration error: failed to get active queue for flow %q: %w", req.FlowID(), err)
+		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))
 		return
 	}
-	priority := managedQ.FlowQueueAccessor().FlowSpec().Priority
-	logger = logger.WithValues("priority", priority)
 
-	band, err := sp.shard.PriorityBandAccessor(priority)
+	band, err := sp.shard.PriorityBandAccessor(key.Priority)
 	if err != nil {
-		finalErr := fmt.Errorf("configuration error: failed to get priority band for priority %d: %w", priority, err)
+		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))
 		return
 	}
 	logger = logger.WithValues("priorityName", band.PriorityName())
 
-	if !sp.hasCapacity(priority, req.ByteSize()) {
+	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[priority]
+		bandStats := stats.PerPriorityBandStats[key.Priority]
 		logger.V(logutil.DEFAULT).Info("Rejecting request, queue at capacity",
 			"outcome", types.QueueOutcomeRejectedCapacity,
 			"shardTotalBytes", stats.TotalByteSize,
@@ -250,15 +251,14 @@ func (sp *ShardProcessor) enqueue(item *flowItem) {
 	//     eventually find and evict any finalized item that slips through this check and is added to a queue.
 	if item.isFinalized() {
 		outcome, err := item.FinalState()
-		logger.V(logutil.VERBOSE).Info("Item finalized before adding to queue, ignoring.",
-			"outcome", outcome, "err", err)
+		logger.V(logutil.VERBOSE).Info("Item finalized before adding to queue, ignoring.", "outcome", outcome, "err", err)
 		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.
 	if err := managedQ.Add(item); err != nil {
-		finalErr := fmt.Errorf("failed to add item to queue for flow %q: %w", req.FlowID(), err)
+		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))
 		return
@@ -309,27 +309,25 @@ func (sp *ShardProcessor) dispatchCycle(ctx context.Context) bool {
 	// This could be abstracted into a third policy tier (e.g., an `InterBandDispatchPolicy`) if more complex scheduling
 	// between bands, such as Weighted Fair Queuing (WFQ), is ever required. For now, strict priority is sufficient.
 	for _, priority := range sp.shard.AllOrderedPriorityLevels() {
-		band, err := sp.shard.PriorityBandAccessor(priority)
+		originalBand, err := sp.shard.PriorityBandAccessor(priority)
 		if err != nil {
 			baseLogger.Error(err, "Failed to get PriorityBandAccessor, skipping band", "priority", priority)
 			continue
 		}
-		logger := baseLogger.WithValues("priority", priority, "priorityName", band.PriorityName())
+		logger := baseLogger.WithValues("priority", priority, "priorityName", originalBand.PriorityName())
 
 		// Apply the configured filter to get a view of only the dispatchable flows.
-		allowedFlows, shouldPause := sp.dispatchFilter(ctx, band, logger)
+		dispatchableBand, shouldPause := sp.dispatchFilter(ctx, originalBand, logger)
 		if shouldPause {
 			return false // A global gate told us to stop the entire cycle.
 		}
-
-		dispatchableBand := band
-		if allowedFlows != nil {
-			// An explicit subset of flows is allowed; create a filtered view.
-			dispatchableBand = newSubsetPriorityBandAccessor(band, allowedFlows)
+		if dispatchableBand == nil {
+			// A nil return from the filter indicates the fast path: no filtering was needed.
+			dispatchableBand = originalBand
 		}
 
 		// Pass the (potentially filtered) band to the policies.
-		item, dispatchPriority, err := sp.selectItem(dispatchableBand, logger)
+		item, err := sp.selectItem(dispatchableBand, logger)
 		if err != nil {
 			// The error handling strategy depends on the type of failure (inter- vs. intra-flow).
 			if errors.Is(err, errIntraFlow) {
@@ -344,9 +342,14 @@ func (sp *ShardProcessor) dispatchCycle(ctx context.Context) bool {
 			logger.V(logutil.TRACE).Info("No item selected by dispatch policies, skipping band")
 			continue
 		}
-		logger = logger.WithValues("flowID", item.OriginalRequest().FlowID(), "reqID", item.OriginalRequest().ID())
-
-		if err := sp.dispatchItem(item, dispatchPriority, logger); err != nil {
+		logger = logger.WithValues(
+			"flowKey", item.OriginalRequest().FlowKey(),
+			"flowID", item.OriginalRequest().FlowKey().ID,
+			"flowPriority", item.OriginalRequest().FlowKey().Priority,
+			"reqID", item.OriginalRequest().ID(),
+			"reqByteSize", item.OriginalRequest().ByteSize())
+
+		if err := sp.dispatchItem(item, logger); err != nil {
 			// All errors from dispatchItem are considered intra-flow and unrecoverable for this band in this cycle.
 			logger.Error(err, "Failed to dispatch item, skipping priority band for this cycle")
 			continue
@@ -363,52 +366,52 @@ func (sp *ShardProcessor) dispatchCycle(ctx context.Context) bool {
 func (sp *ShardProcessor) selectItem(
 	band framework.PriorityBandAccessor,
 	logger logr.Logger,
-) (types.QueueItemAccessor, uint, error) {
+) (types.QueueItemAccessor, error) {
 	interP, err := sp.shard.InterFlowDispatchPolicy(band.Priority())
 	if err != nil {
-		return nil, 0, fmt.Errorf("%w: could not get InterFlowDispatchPolicy: %w", errInterFlow, err)
+		return nil, fmt.Errorf("%w: could not get InterFlowDispatchPolicy: %w", errInterFlow, err)
 	}
 	queue, err := interP.SelectQueue(band)
 	if err != nil {
-		return nil, 0, fmt.Errorf("%w: InterFlowDispatchPolicy %q failed to select queue: %w",
+		return nil, fmt.Errorf("%w: InterFlowDispatchPolicy %q failed to select queue: %w",
 			errInterFlow, interP.Name(), err)
 	}
 	if queue == nil {
 		logger.V(logutil.TRACE).Info("No queue selected by InterFlowDispatchPolicy")
-		return nil, 0, nil
-	}
-	logger = logger.WithValues("selectedFlowID", queue.FlowSpec().ID)
-
-	priority := queue.FlowSpec().Priority
-	intraP, err := sp.shard.IntraFlowDispatchPolicy(queue.FlowSpec().ID, priority)
+		return nil, nil
+	}
+	key := queue.FlowKey()
+	logger = logger.WithValues(
+		"selectedFlowKey", key,
+		"selectedFlowID", key.ID,
+		"selectedFlowPriority", key.Priority)
+	intraP, err := sp.shard.IntraFlowDispatchPolicy(key)
 	if err != nil {
 		// This is an intra-flow failure because we have already successfully selected a queue.
-		return nil, 0, fmt.Errorf("%w: could not get IntraFlowDispatchPolicy for flow %q: %w",
-			errIntraFlow, queue.FlowSpec().ID, err)
+		return nil, fmt.Errorf("%w: could not get IntraFlowDispatchPolicy for flow %q: %w", errIntraFlow, key, err)
 	}
 	item, err := intraP.SelectItem(queue)
 	if err != nil {
-		return nil, 0, fmt.Errorf("%w: IntraFlowDispatchPolicy %q failed to select item for flow %q: %w",
-			errIntraFlow, intraP.Name(), queue.FlowSpec().ID, err)
+		return nil, fmt.Errorf("%w: IntraFlowDispatchPolicy %q failed to select item for flow %q: %w",
+			errIntraFlow, intraP.Name(), key, err)
 	}
 	if item == nil {
 		logger.V(logutil.TRACE).Info("No item selected by IntraFlowDispatchPolicy")
-		return nil, 0, nil
+		return nil, nil
 	}
-	return item, priority, nil
+	return item, nil
 }
 
 // dispatchItem handles the final steps of dispatching an item after it has been selected by policies. This includes
 // removing it from its queue, checking for last-minute expiry, and finalizing its outcome.
-func (sp *ShardProcessor) dispatchItem(itemAcc types.QueueItemAccessor, priority uint, logger logr.Logger) error {
+func (sp *ShardProcessor) dispatchItem(itemAcc types.QueueItemAccessor, logger logr.Logger) error {
 	logger = logger.WithName("dispatchItem")
 
 	req := itemAcc.OriginalRequest()
 	// We must look up the queue by its specific priority, as a flow might have draining queues at other levels.
-	managedQ, err := sp.shard.ManagedQueue(req.FlowID(), priority)
+	managedQ, err := sp.shard.ManagedQueue(req.FlowKey())
 	if err != nil {
-		return fmt.Errorf("%w: failed to get ManagedQueue for flow %q at priority %d: %w",
-			errIntraFlow, req.FlowID(), priority, err)
+		return fmt.Errorf("%w: failed to get ManagedQueue for flow %q: %w", errIntraFlow, req.FlowKey(), err)
 	}
 
 	// The core mutation: remove the item from the queue.
@@ -418,7 +421,7 @@ func (sp *ShardProcessor) dispatchItem(itemAcc types.QueueItemAccessor, priority
 		// selected by the policy and the time this function is called.
 		logger.V(logutil.VERBOSE).Info("Item already removed from queue, likely by expiry cleanup", "err", err)
 		return fmt.Errorf("%w: failed to remove item %q from queue for flow %q: %w",
-			errIntraFlow, req.ID(), req.FlowID(), err)
+			errIntraFlow, req.ID(), req.FlowKey(), err)
 	}
 
 	removedItem, ok := removedItemAcc.(*flowItem)
@@ -625,8 +628,12 @@ func (sp *ShardProcessor) processAllQueuesConcurrently(
 		go func() {
 			defer wg.Done()
 			for q := range tasks {
-				queueLogger := logger.WithValues("flowID", q.FlowSpec().ID, "priority", q.FlowSpec().Priority)
-				managedQ, err := sp.shard.ManagedQueue(q.FlowSpec().ID, q.FlowSpec().Priority)
+				key := q.FlowKey()
+				queueLogger := logger.WithValues(
+					"flowKey", key,
+					"flowID", key.ID,
+					"flowPriority", key.Priority)
+				managedQ, err := sp.shard.ManagedQueue(key)
 				if err != nil {
 					queueLogger.Error(err, "Failed to get ManagedQueue")
 					continue
diff --git a/pkg/epp/flowcontrol/framework/mocks/mocks.go b/pkg/epp/flowcontrol/framework/mocks/mocks.go
index 804ba71..b8715b7 100644
--- a/pkg/epp/flowcontrol/framework/mocks/mocks.go
+++ b/pkg/epp/flowcontrol/framework/mocks/mocks.go
@@ -41,7 +41,7 @@ type MockFlowQueueAccessor struct {
 	PeekHeadErrV  error
 	PeekTailV     types.QueueItemAccessor
 	PeekTailErrV  error
-	FlowSpecV     types.FlowSpecification
+	FlowKeyV      types.FlowKey
 	ComparatorV   framework.ItemComparator
 	CapabilitiesV []framework.QueueCapability
 }
@@ -50,7 +50,7 @@ func (m *MockFlowQueueAccessor) Name() string                              { ret
 func (m *MockFlowQueueAccessor) Len() int                                  { return m.LenV }
 func (m *MockFlowQueueAccessor) ByteSize() uint64                          { return m.ByteSizeV }
 func (m *MockFlowQueueAccessor) Comparator() framework.ItemComparator      { return m.ComparatorV }
-func (m *MockFlowQueueAccessor) FlowSpec() types.FlowSpecification         { return m.FlowSpecV }
+func (m *MockFlowQueueAccessor) FlowKey() types.FlowKey                    { return m.FlowKeyV }
 func (m *MockFlowQueueAccessor) Capabilities() []framework.QueueCapability { return m.CapabilitiesV }
 
 func (m *MockFlowQueueAccessor) PeekHead() (types.QueueItemAccessor, error) {
@@ -69,7 +69,7 @@ var _ framework.FlowQueueAccessor = &MockFlowQueueAccessor{}
 type MockPriorityBandAccessor struct {
 	PriorityV         uint
 	PriorityNameV     string
-	FlowIDsFunc       func() []string
+	FlowKeysFunc      func() []types.FlowKey
 	QueueFunc         func(flowID string) framework.FlowQueueAccessor
 	IterateQueuesFunc func(callback func(queue framework.FlowQueueAccessor) (keepIterating bool))
 }
@@ -77,16 +77,16 @@ type MockPriorityBandAccessor struct {
 func (m *MockPriorityBandAccessor) Priority() uint       { return m.PriorityV }
 func (m *MockPriorityBandAccessor) PriorityName() string { return m.PriorityNameV }
 
-func (m *MockPriorityBandAccessor) FlowIDs() []string {
-	if m.FlowIDsFunc != nil {
-		return m.FlowIDsFunc()
+func (m *MockPriorityBandAccessor) FlowKeys() []types.FlowKey {
+	if m.FlowKeysFunc != nil {
+		return m.FlowKeysFunc()
 	}
 	return nil
 }
 
-func (m *MockPriorityBandAccessor) Queue(flowID string) framework.FlowQueueAccessor {
+func (m *MockPriorityBandAccessor) Queue(id string) framework.FlowQueueAccessor {
 	if m.QueueFunc != nil {
-		return m.QueueFunc(flowID)
+		return m.QueueFunc(id)
 	}
 	return nil
 }
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin.go b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin.go
index 9906dc1..c71b801 100644
--- a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin.go
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin.go
@@ -24,6 +24,7 @@ import (
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
 )
 
 // RoundRobinPolicyName is the name of the Round Robin policy implementation.
@@ -70,7 +71,7 @@ func (p *roundRobin) SelectQueue(band framework.PriorityBandAccessor) (framework
 // "RoundRobin" displacement policy is introduced, while keeping the dispatch policy's public API stable.
 type iterator struct {
 	mu           sync.Mutex
-	lastSelected string
+	lastSelected *types.FlowKey
 }
 
 // newIterator creates a new round-robin Iterator.
@@ -85,34 +86,35 @@ func (r *iterator) selectNextQueue(band framework.PriorityBandAccessor) framewor
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
-	flowIDs := band.FlowIDs()
-	if len(flowIDs) == 0 {
-		r.lastSelected = "" // Reset state if no flows are present
+	keys := band.FlowKeys()
+	if len(keys) == 0 {
+		r.lastSelected = nil // Reset state if no flows are present
 		return nil
 	}
-	slices.Sort(flowIDs)
+	// Sort for deterministic ordering.
+	slices.SortFunc(keys, func(a, b types.FlowKey) int { return a.Compare(b) })
 
 	startIndex := 0
-	if r.lastSelected != "" {
+	if r.lastSelected != nil {
 		// Find the index of the last selected flow.
 		// If it's not found (e.g., the flow was removed), we'll start from the beginning.
-		if idx := slices.Index(flowIDs, r.lastSelected); idx != -1 {
-			startIndex = (idx + 1) % len(flowIDs)
+		if idx := slices.Index(keys, *r.lastSelected); idx != -1 {
+			startIndex = (idx + 1) % len(keys)
 		}
 	}
 
-	numFlows := len(flowIDs)
+	numFlows := len(keys)
 	for i := 0; i < numFlows; i++ {
 		currentIdx := (startIndex + i) % numFlows
-		currentFlowID := flowIDs[currentIdx]
-		queue := band.Queue(currentFlowID)
+		currentKey := keys[currentIdx]
+		queue := band.Queue(currentKey.ID)
 		if queue != nil && queue.Len() > 0 {
-			r.lastSelected = currentFlowID
+			r.lastSelected = &currentKey
 			return queue
 		}
 	}
 
 	// No non-empty queue was found.
-	r.lastSelected = ""
+	r.lastSelected = nil
 	return nil
 }
diff --git a/pkg/epp/flowcontrol/framework/policies.go b/pkg/epp/flowcontrol/framework/policies.go
index da6d5c4..5fc3e64 100644
--- a/pkg/epp/flowcontrol/framework/policies.go
+++ b/pkg/epp/flowcontrol/framework/policies.go
@@ -144,8 +144,8 @@ type InterFlowDispatchPolicy interface {
 // FlowQueueAccessor provides a policy-facing, read-only view of a single flow's queue.
 // It combines general queue inspection methods (embedded via `QueueInspectionMethods`) with flow-specific metadata.
 //
-// Instances of `FlowQueueAccessor` are vended by a `contracts.ManagedQueue` and are the primary means by which policies
-// inspect individual queue state.
+// Instances of `FlowQueueAccessor` are typically obtained via a `PriorityBandAccessor` and are the primary means by
+// which policies inspect individual queue state.
 //
 // Conformance: Implementations MUST ensure all methods (including those embedded from `QueueInspectionMethods`) are
 // goroutine-safe for concurrent access.
@@ -156,12 +156,12 @@ type FlowQueueAccessor interface {
 	// This is determined by the `IntraFlowDispatchPolicy` associated with this queue's flow.
 	Comparator() ItemComparator
 
-	// FlowSpec returns the `types.FlowSpecification` of the flow this queue accessor is associated with.
-	// This provides essential context (like `FlowID`) to policies.
-	FlowSpec() types.FlowSpecification
+	// FlowKey returns the unique, immutable `types.FlowKey` of the flow instance this queue accessor is associated with.
+	// This provides essential context (like the logical grouping `ID` and `Priority`) to policies.
+	FlowKey() types.FlowKey
 }
 
-// PriorityBandAccessor provides a read-only view into a specific priority band within the `ports.FlowRegistry`.
+// PriorityBandAccessor provides a read-only view into a specific priority band within the `contracts.FlowRegistry`.
 // It allows the `controller.FlowController` and inter-flow policies to inspect the state of all flow queues within that
 // band.
 //
@@ -173,15 +173,20 @@ type PriorityBandAccessor interface {
 	// PriorityName returns the human-readable name of this priority band.
 	PriorityName() string
 
-	// FlowIDs returns a slice of all flow IDs within this priority band.
-	// The order of items in the slice is not guaranteed unless specified by the implementations (e.g., for deterministic
-	// testing scenarios).
-	FlowIDs() []string
+	// FlowKeys returns a slice of the composite `types.FlowKey`s for every flow instance currently active within this
+	// priority band.
+	// This method provides the complete, canonical identifiers for all flows in the band. The caller can use the `ID`
+	// field from each key to look up a specific queue via the `Queue(id string)` method.
+	// The order of keys in the returned slice is not guaranteed unless specified by the implementations (e.g., for
+	// deterministic testing scenarios).
+	FlowKeys() []types.FlowKey
 
-	// Queue returns a `FlowQueueAccessor` for the specified `flowID` within this priority band.
+	// Queue returns a `FlowQueueAccessor` for the specified logical grouping `ID` within this priority band.
+	// Note: This uses the logical `ID` string (`types.FlowKey.ID`), not the full `types.FlowKey`, as the priority is
+	// already defined by the accessor's scope.
 	//
-	// Conformance: Implementations MUST return nil if the `flowID` is not found in this band.
-	Queue(flowID string) FlowQueueAccessor
+	// Conformance: Implementations MUST return nil if the `ID` is not found in this band.
+	Queue(id string) FlowQueueAccessor
 
 	// IterateQueues executes the given `callback` for each `FlowQueueAccessor` in this priority band.
 	// Iteration stops if the `callback` returns false. The order of iteration is not guaranteed unless specified by the
diff --git a/pkg/epp/flowcontrol/registry/doc.go b/pkg/epp/flowcontrol/registry/doc.go
index 521ad6e..a038d3d 100644
--- a/pkg/epp/flowcontrol/registry/doc.go
+++ b/pkg/epp/flowcontrol/registry/doc.go
@@ -32,7 +32,7 @@ limitations under the License.
 //
 //   - `managedQueue`: A concrete implementation of the `contracts.ManagedQueue` interface. It acts as a stateful
 //     decorator around a `framework.SafeQueue`, adding critical registry-level functionality such as atomic statistics
-//     tracking and lifecycle state enforcement (active vs. draining).
+//     tracking.
 //
 //   - `Config`: The top-level configuration object that defines the structure and default behaviors of the registry,
 //     including the definition of priority bands and default policy selections. This configuration is partitioned and
diff --git a/pkg/epp/flowcontrol/registry/managedqueue.go b/pkg/epp/flowcontrol/registry/managedqueue.go
index bea7e7e..b52adb1 100644
--- a/pkg/epp/flowcontrol/registry/managedqueue.go
+++ b/pkg/epp/flowcontrol/registry/managedqueue.go
@@ -17,7 +17,6 @@ limitations under the License.
 package registry
 
 import (
-	"fmt"
 	"sync/atomic"
 
 	"github.com/go-logr/logr"
@@ -33,12 +32,8 @@ import (
 type parentStatsReconciler func(lenDelta, byteSizeDelta int64)
 
 // managedQueue implements `contracts.ManagedQueue`. It is a stateful decorator that wraps a `framework.SafeQueue`,
-// augmenting it with two critical, registry-level responsibilities:
-//  1. Atomic Statistics: It maintains its own `len` and `byteSize` counters, which are updated atomically. This allows
-//     the parent `registryShard` to aggregate statistics across many queues without locks.
-//  2. Lifecycle Enforcement: It tracks the queue's lifecycle state (active vs. draining) via an `isActive` flag. This
-//     is crucial for graceful flow updates, as it allows the registry to stop new requests from being enqueued while
-//     allowing existing items to be drained.
+// augmenting it with atomic statistics tracking. This allows the parent `registryShard` to aggregate statistics across
+// many queues without locks.
 //
 // # Statistical Integrity
 //
@@ -60,10 +55,9 @@ type managedQueue struct {
 	// atomic operations on the stats fields.
 	queue               framework.SafeQueue
 	dispatchPolicy      framework.IntraFlowDispatchPolicy
-	flowSpec            types.FlowSpecification
+	flowKey             types.FlowKey
 	byteSize            atomic.Uint64
 	len                 atomic.Uint64
-	isActive            atomic.Bool
 	reconcileShardStats parentStatsReconciler
 	logger              logr.Logger
 }
@@ -72,48 +66,35 @@ type managedQueue struct {
 func newManagedQueue(
 	queue framework.SafeQueue,
 	dispatchPolicy framework.IntraFlowDispatchPolicy,
-	flowSpec types.FlowSpecification,
+	flowKey types.FlowKey,
 	logger logr.Logger,
 	reconcileShardStats parentStatsReconciler,
 ) *managedQueue {
 	mqLogger := logger.WithName("managed-queue").WithValues(
-		"flowID", flowSpec.ID,
-		"priority", flowSpec.Priority,
+		"flowKey", flowKey,
+		"flowID", flowKey.ID,
+		"priority", flowKey.Priority,
 		"queueType", queue.Name(),
 	)
 	mq := &managedQueue{
 		queue:               queue,
 		dispatchPolicy:      dispatchPolicy,
-		flowSpec:            flowSpec,
+		flowKey:             flowKey,
 		reconcileShardStats: reconcileShardStats,
 		logger:              mqLogger,
 	}
-	mq.isActive.Store(true)
 	return mq
 }
 
-// markAsDraining is an internal method called by the parent shard to transition this queue to a draining state.
-// Once a queue is marked as draining, it will no longer accept new items via `Add`.
-func (mq *managedQueue) markAsDraining() {
-	// Use CompareAndSwap to ensure we only log the transition once.
-	if mq.isActive.CompareAndSwap(true, false) {
-		mq.logger.V(logging.DEFAULT).Info("Queue marked as draining")
-	}
-}
-
 // FlowQueueAccessor returns a new `flowQueueAccessor` instance, which provides a read-only, policy-facing view of the
 // queue.
 func (mq *managedQueue) FlowQueueAccessor() framework.FlowQueueAccessor {
 	return &flowQueueAccessor{mq: mq}
 }
 
-// Add first checks if the queue is active. If it is, it wraps the underlying `framework.SafeQueue.Add` call and
-// atomically updates the queue's and the parent shard's statistics.
+// Add wraps the underlying `framework.SafeQueue.Add` call and atomically updates the queue's and the parent shard's
+// statistics.
 func (mq *managedQueue) Add(item types.QueueItemAccessor) error {
-	if !mq.isActive.Load() {
-		return fmt.Errorf("flow instance %q is not active and cannot accept new requests: %w",
-			mq.flowSpec.ID, contracts.ErrFlowInstanceNotFound)
-	}
 	if err := mq.queue.Add(item); err != nil {
 		return err
 	}
@@ -240,7 +221,7 @@ func (a *flowQueueAccessor) PeekTail() (types.QueueItemAccessor, error) { return
 // Comparator returns the `framework.ItemComparator` that defines this queue's item ordering logic.
 func (a *flowQueueAccessor) Comparator() framework.ItemComparator { return a.mq.Comparator() }
 
-// FlowSpec returns the `types.FlowSpecification` of the flow this queue accessor is associated with.
-func (a *flowQueueAccessor) FlowSpec() types.FlowSpecification { return a.mq.flowSpec }
+// FlowKey returns the `types.FlowKey` of the flow this queue accessor is associated with.
+func (a *flowQueueAccessor) FlowKey() types.FlowKey { return a.mq.flowKey }
 
 var _ framework.FlowQueueAccessor = &flowQueueAccessor{}
diff --git a/pkg/epp/flowcontrol/registry/shard.go b/pkg/epp/flowcontrol/registry/shard.go
index 1e894f0..eb11dc7 100644
--- a/pkg/epp/flowcontrol/registry/shard.go
+++ b/pkg/epp/flowcontrol/registry/shard.go
@@ -28,6 +28,7 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
 	inter "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch"
 	intra "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
@@ -43,8 +44,8 @@ import (
 // # Concurrency
 //
 // The `registryShard` uses a combination of an `RWMutex` and atomic operations to manage concurrency.
-//   - The `mu` RWMutex protects the shard's internal maps (`priorityBands`, `activeFlows`) during administrative
-//     operations like flow registration or updates. This ensures that the set of active or draining queues appears
+//   - The `mu` RWMutex protects the shard's internal maps (`priorityBands`) during administrative
+//     operations like flow registration or updates. This ensures that the set of queues appears
 //     atomic to a `controller.FlowController` worker. All read-oriented methods on the shard take a read lock.
 //   - All statistics (`totalByteSize`, `totalLen`, etc.) are implemented as `atomic.Uint64` to allow for lock-free,
 //     high-performance updates from many concurrent queue operations.
@@ -55,18 +56,12 @@ type registryShard struct {
 	isActive     bool
 	reconcileFun parentStatsReconciler
 
-	// mu protects the shard's internal maps (`priorityBands` and `activeFlows`).
+	// mu protects the shard's internal maps (`priorityBands`).
 	mu sync.RWMutex
 
-	// priorityBands is the primary lookup table for all managed queues on this shard, organized by `priority`, then by
-	// `flowID`. This map contains BOTH active and draining queues.
+	// priorityBands is the primary lookup table for all managed queues on this shard, organized by `priority`.
 	priorityBands map[uint]*priorityBand
 
-	// activeFlows is a flattened map for O(1) access to the SINGLE active queue for a given logical flow ID.
-	// This is the critical lookup for the `Enqueue` path. If a `flowID` is not in this map, it has no active queue on
-	// this shard.
-	activeFlows map[string]*managedQueue
-
 	// orderedPriorityLevels is a cached, sorted list of `priority` levels.
 	// It is populated at initialization to avoid repeated map key iteration and sorting during the dispatch loop,
 	// ensuring a deterministic, ordered traversal from highest to lowest priority.
@@ -82,9 +77,12 @@ type priorityBand struct {
 	// config holds the partitioned config for this specific band.
 	config PriorityBandConfig
 
-	// queues holds all `managedQueue` instances within this band, keyed by `flowID`. This includes both active and
-	// draining queues.
-	queues map[string]*managedQueue
+	// queues holds all `managedQueue` instances within this band, keyed by their composite `FlowKey`.
+	queues map[types.FlowKey]*managedQueue
+
+	// flowKeys is a cached slice of the keys from the `queues` map. This is an optimization to avoid repeated map key
+	// iteration in the `FlowKeys()` accessor method, making it an O(1) operation.
+	flowKeys []types.FlowKey
 
 	// Band-level statistics, which are updated atomically.
 	byteSize atomic.Uint64
@@ -110,7 +108,6 @@ func newShard(
 		isActive:      true,
 		reconcileFun:  reconcileFunc,
 		priorityBands: make(map[uint]*priorityBand, len(partitionedConfig.PriorityBands)),
-		activeFlows:   make(map[string]*managedQueue),
 	}
 
 	for _, bandConfig := range partitionedConfig.PriorityBands {
@@ -128,7 +125,7 @@ func newShard(
 
 		s.priorityBands[bandConfig.Priority] = &priorityBand{
 			config:                         bandConfig,
-			queues:                         make(map[string]*managedQueue),
+			queues:                         make(map[types.FlowKey]*managedQueue),
 			interFlowDispatchPolicy:        interPolicy,
 			defaultIntraFlowDispatchPolicy: intraPolicy,
 		}
@@ -171,51 +168,36 @@ func (s *registryShard) IsActive() bool {
 	return s.isActive
 }
 
-// ActiveManagedQueue returns the currently active `ManagedQueue` for a given flow.
-func (s *registryShard) ActiveManagedQueue(flowID string) (contracts.ManagedQueue, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	mq, ok := s.activeFlows[flowID]
-	if !ok {
-		return nil, fmt.Errorf("failed to get active queue for flow %q: %w", flowID, contracts.ErrFlowInstanceNotFound)
-	}
-	return mq, nil
-}
-
-// ManagedQueue retrieves a specific (potentially draining) `ManagedQueue` instance from this shard.
-func (s *registryShard) ManagedQueue(flowID string, priority uint) (contracts.ManagedQueue, error) {
+// ManagedQueue retrieves a specific `ManagedQueue` instance from this shard.
+func (s *registryShard) ManagedQueue(key types.FlowKey) (contracts.ManagedQueue, error) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
-	band, ok := s.priorityBands[priority]
+	band, ok := s.priorityBands[key.Priority]
 	if !ok {
-		return nil, fmt.Errorf("failed to get managed queue for flow %q: %w", flowID, contracts.ErrPriorityBandNotFound)
+		return nil, fmt.Errorf("failed to get managed queue for flow %q: %w", key, contracts.ErrPriorityBandNotFound)
 	}
-	mq, ok := band.queues[flowID]
+	mq, ok := band.queues[key]
 	if !ok {
-		return nil, fmt.Errorf("failed to get managed queue for flow %q at priority %d: %w",
-			flowID, priority, contracts.ErrFlowInstanceNotFound)
+		return nil, fmt.Errorf("failed to get managed queue for flow %q: %w",
+			key, contracts.ErrFlowInstanceNotFound)
 	}
 	return mq, nil
 }
 
 // IntraFlowDispatchPolicy retrieves a flow's configured `framework.IntraFlowDispatchPolicy`.
-func (s *registryShard) IntraFlowDispatchPolicy(
-	flowID string,
-	priority uint,
-) (framework.IntraFlowDispatchPolicy, error) {
+func (s *registryShard) IntraFlowDispatchPolicy(key types.FlowKey) (framework.IntraFlowDispatchPolicy, error) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
-	band, ok := s.priorityBands[priority]
+	band, ok := s.priorityBands[key.Priority]
 	if !ok {
-		return nil, fmt.Errorf("failed to get intra-flow policy for flow %q: %w", flowID, contracts.ErrPriorityBandNotFound)
+		return nil, fmt.Errorf("failed to get intra-flow policy for flow %q: %w", key, contracts.ErrPriorityBandNotFound)
 	}
-	mq, ok := band.queues[flowID]
+	mq, ok := band.queues[key]
 	if !ok {
-		return nil, fmt.Errorf("failed to get intra-flow policy for flow %q at priority %d: %w",
-			flowID, priority, contracts.ErrFlowInstanceNotFound)
+		return nil, fmt.Errorf("failed to get intra-flow policy for flow %q: %w",
+			key, contracts.ErrFlowInstanceNotFound)
 	}
 	// The policy is stored on the managed queue.
 	return mq.dispatchPolicy, nil
@@ -299,24 +281,20 @@ func (a *priorityBandAccessor) PriorityName() string {
 	return a.band.config.PriorityName
 }
 
-// FlowIDs returns a slice of all flow IDs within this priority band.
-func (a *priorityBandAccessor) FlowIDs() []string {
+// FlowKeys returns a slice of all flow keys within this priority band.
+// This is an O(1) operation because the slice is pre-computed and cached.
+func (a *priorityBandAccessor) FlowKeys() []types.FlowKey {
 	a.shard.mu.RLock()
 	defer a.shard.mu.RUnlock()
-
-	flowIDs := make([]string, 0, len(a.band.queues))
-	for id := range a.band.queues {
-		flowIDs = append(flowIDs, id)
-	}
-	return flowIDs
+	return a.band.flowKeys
 }
 
-// Queue returns a `framework.FlowQueueAccessor` for the specified `flowID` within this priority band.
-func (a *priorityBandAccessor) Queue(flowID string) framework.FlowQueueAccessor {
+// Queue returns a `framework.FlowQueueAccessor` for the specified `ID` within this priority band.
+func (a *priorityBandAccessor) Queue(id string) framework.FlowQueueAccessor {
 	a.shard.mu.RLock()
 	defer a.shard.mu.RUnlock()
 
-	mq, ok := a.band.queues[flowID]
+	mq, ok := a.band.queues[types.FlowKey{ID: id, Priority: a.Priority()}]
 	if !ok {
 		return nil
 	}
@@ -330,7 +308,8 @@ func (a *priorityBandAccessor) IterateQueues(callback func(queue framework.FlowQ
 	a.shard.mu.RLock()
 	defer a.shard.mu.RUnlock()
 
-	for _, mq := range a.band.queues {
+	for _, key := range a.band.flowKeys {
+		mq := a.band.queues[key]
 		if !callback(mq.FlowQueueAccessor()) {
 			return
 		}
diff --git a/pkg/epp/flowcontrol/types/flow.go b/pkg/epp/flowcontrol/types/flow.go
index 4bdd508..0d8a1b8 100644
--- a/pkg/epp/flowcontrol/types/flow.go
+++ b/pkg/epp/flowcontrol/types/flow.go
@@ -16,13 +16,63 @@ limitations under the License.
 
 package types
 
-// FlowSpecification defines the complete configuration for a single logical flow.
-// It is the data contract used by the `contracts.FlowRegistry` to create and manage the lifecycle of queues and
-// policies.
-type FlowSpecification struct {
-	// ID is the unique identifier for this flow (e.g., model name, tenant ID).
+import (
+	"fmt"
+	"strings"
+)
+
+// FlowKey is the unique, immutable identifier for a single, independently managed flow instance within the
+// `contracts.FlowRegistry`.
+// It combines a logical grouping `ID` with a specific `Priority` level to form a composite primary key.
+//
+// The core architectural principle of this model is that each unique, immutable `FlowKey` represents a completely
+// separate stream of traffic with its own queue, lifecycle and statistics.
+type FlowKey struct {
+	// ID is the logical grouping identifier for a flow, such as a tenant ID or a model name. This ID can be shared across
+	// multiple `FlowKey`s to represent different classes of traffic for the same logical entity. It provides a way to
+	// group related traffic but does not uniquely identify a manageable flow instance on its own.
 	ID string
 
-	// Priority is the numerical priority level for this flow. Lower values indicate higher priority.
+	// Priority is the numerical priority level that defines the priority band for this specific flow instance.
+	//
+	// A different Priority value for the same ID creates a distinct flow instance. For example, the keys
+	// `{ID: "TenantA", Priority: 10}` and `{ID: "TenantA", Priority: 100}` are treated as two completely separate,
+	// concurrently active flows. This allows a single tenant to serve traffic at multiple priority levels simultaneously.
+	//
+	// Because the `FlowKey` is immutable, changing the priority of traffic requires using a new `FlowKey`; the old flow
+	// instance will be automatically garbage collected by the registry when it becomes idle.
 	Priority uint
 }
+
+// Compare provides a stable comparison function for two FlowKey instances, suitable for use with sorting algorithms.
+// It returns -1 if the key is less than the other, 0 if they are equal, and 1 if the key is greater than the other.
+// The comparison is performed first by Priority (descending) and then by ID (ascending).
+func (k FlowKey) Compare(other FlowKey) int {
+	if k.Priority > other.Priority {
+		return -1
+	}
+	if k.Priority < other.Priority {
+		return 1
+	}
+	return strings.Compare(k.ID, other.ID)
+}
+
+func (k FlowKey) String() string {
+	return fmt.Sprintf("%s::%d", k.ID, k.Priority)
+}
+
+// FlowSpecification defines the complete configuration for a single logical flow instance, which is uniquely identified
+// by its immutable `Key`.
+//
+// It is the primary data contract used by the `contracts.FlowRegistry` to register or update a flow instance. The
+// registry manages one flow instance for each unique `FlowKey`. While the `Key` itself is immutable, the specification
+// can be updated via `RegisterOrUpdateFlow` to modify configurable parameters (e.g., policies, capacity limits) for the
+// existing flow instance (when these update stories are supported).
+type FlowSpecification struct {
+	// Key is the unique, immutable identifier for the flow instance this specification describes.
+	Key FlowKey
+
+	// TODO: Add other flow-scoped configuration fields here, such as:
+	// - IntraFlowDispatchPolicy intra.RegisteredPolicyName
+	// - CapacityBytes uint64
+}
diff --git a/pkg/epp/flowcontrol/types/mocks/mocks.go b/pkg/epp/flowcontrol/types/mocks/mocks.go
index 7eb6f37..dbef031 100644
--- a/pkg/epp/flowcontrol/types/mocks/mocks.go
+++ b/pkg/epp/flowcontrol/types/mocks/mocks.go
@@ -28,27 +28,32 @@ import (
 // MockFlowControlRequest provides a mock implementation of the `types.FlowControlRequest` interface.
 type MockFlowControlRequest struct {
 	Ctx                  context.Context
-	FlowIDV              string
+	FlowKeyV             types.FlowKey
 	ByteSizeV            uint64
 	InitialEffectiveTTLV time.Duration
 	IDV                  string
 }
 
 // NewMockFlowControlRequest creates a new `MockFlowControlRequest` instance.
-func NewMockFlowControlRequest(byteSize uint64, id, flowID string, ctx context.Context) *MockFlowControlRequest {
+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,
-		FlowIDV:   flowID,
+		FlowKeyV:  key,
 		Ctx:       ctx,
 	}
 }
 
 func (m *MockFlowControlRequest) Context() context.Context           { return m.Ctx }
-func (m *MockFlowControlRequest) FlowID() string                     { return m.FlowIDV }
+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 }
 func (m *MockFlowControlRequest) ID() string                         { return m.IDV }
@@ -92,13 +97,13 @@ var _ types.QueueItemAccessor = &MockQueueItemAccessor{}
 
 // NewMockQueueItemAccessor is a constructor for `MockQueueItemAccessor` that initializes the mock with a default
 // `MockFlowControlRequest` and `MockQueueItemHandle` to prevent nil pointer dereferences in tests.
-func NewMockQueueItemAccessor(byteSize uint64, reqID, flowID string) *MockQueueItemAccessor {
+func NewMockQueueItemAccessor(byteSize uint64, reqID string, key types.FlowKey) *MockQueueItemAccessor {
 	return &MockQueueItemAccessor{
 		EnqueueTimeV: time.Now(),
 		OriginalRequestV: NewMockFlowControlRequest(
 			byteSize,
 			reqID,
-			flowID,
+			key,
 			context.Background(),
 		),
 		HandleV: &MockQueueItemHandle{},
diff --git a/pkg/epp/flowcontrol/types/request.go b/pkg/epp/flowcontrol/types/request.go
index 8b50236..7569407 100644
--- a/pkg/epp/flowcontrol/types/request.go
+++ b/pkg/epp/flowcontrol/types/request.go
@@ -33,10 +33,11 @@ type FlowControlRequest interface {
 	// queue.
 	Context() context.Context
 
-	// FlowID returns the unique identifier for the flow this request belongs to (e.g., model name, tenant ID). The
-	// `controller.FlowController` uses this ID to look up the active `contracts.ManagedQueue` and configured
-	// `framework.IntraFlowDispatchPolicy` from a `contracts.RegistryShard`.
-	FlowID() string
+	// 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`.
+	// The returned key is treated as an immutable value.
+	FlowKey() FlowKey
 
 	// ByteSize returns the request's size in bytes (e.g., prompt size). This is used by the `controller.FlowController`
 	// for managing byte-based capacity limits and for `contracts.FlowRegistry` statistics.
diff --git a/pkg/epp/handlers/request.go b/pkg/epp/handlers/request.go
index 096f0c8..7f81221 100644
--- a/pkg/epp/handlers/request.go
+++ b/pkg/epp/handlers/request.go
@@ -17,6 +17,7 @@ limitations under the License.
 package handlers
 
 import (
+	"fmt"
 	"strconv"
 	"time"
 
@@ -45,7 +46,10 @@ func (s *StreamingServer) HandleRequestHeaders(reqCtx *RequestContext, req *extP
 		if err != nil {
 			return err
 		}
-		reqCtx.TargetEndpoint = pod.Address + ":" + strconv.Itoa(int(pool.Spec.TargetPortNumber))
+		if len(pool.Spec.TargetPorts) != 1 {
+			return fmt.Errorf("expected 1 target port, got %d", len(pool.Spec.TargetPorts))
+		}
+		reqCtx.TargetEndpoint = pod.Address + ":" + strconv.Itoa(int(pool.Spec.TargetPorts[0].Number))
 		reqCtx.RequestSize = 0
 		reqCtx.reqHeaderResp = s.generateRequestHeaderResponse(reqCtx)
 		return nil
@@ -69,6 +73,11 @@ func (s *StreamingServer) HandleRequestHeaders(reqCtx *RequestContext, req *extP
 			// remove the objective header from the request headers,
 			// this is not data that should be manipulated or sent to the backend.
 			delete(reqCtx.Request.Headers, header.Key)
+		case metadata.ModelNameRewriteKey:
+			reqCtx.TargetModelName = reqCtx.Request.Headers[header.Key]
+			// remove the rewrite header from the request headers,
+			// this is not data that should be manipulated or sent to the backend.
+			delete(reqCtx.Request.Headers, header.Key)
 		}
 	}
 	return nil
diff --git a/pkg/epp/metadata/consts.go b/pkg/epp/metadata/consts.go
index faf9015..be6c482 100644
--- a/pkg/epp/metadata/consts.go
+++ b/pkg/epp/metadata/consts.go
@@ -30,4 +30,6 @@ const (
 	FlowFairnessIDKey = "x-gateway-inference-fairness-id"
 	// ObjectiveKey is the header key used to specify the objective of an incoming request.
 	ObjectiveKey = "x-gateway-inference-objective"
+	// ModelNameRewriteKey is the header key used to specify the model name to be used when the request is forwarded to the model server.
+	ModelNameRewriteKey = "x-gateway-model-name-rewrite"
 )
diff --git a/pkg/epp/metrics/collectors/inference_pool.go b/pkg/epp/metrics/collectors/inference_pool.go
index 41a3d6d..ec3def1 100644
--- a/pkg/epp/metrics/collectors/inference_pool.go
+++ b/pkg/epp/metrics/collectors/inference_pool.go
@@ -63,7 +63,7 @@ func (c *inferencePoolMetricsCollector) Collect(ch chan<- prometheus.Metric) {
 		return
 	}
 
-	podMetrics := c.ds.PodList(backendmetrics.AllPodPredicate)
+	podMetrics := c.ds.PodList(backendmetrics.AllPodsPredicate)
 	if len(podMetrics) == 0 {
 		return
 	}
diff --git a/pkg/epp/plugins/plugin_state.go b/pkg/epp/plugins/plugin_state.go
new file mode 100644
index 0000000..2da3557
--- /dev/null
+++ b/pkg/epp/plugins/plugin_state.go
@@ -0,0 +1,147 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plugins
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+const (
+	// stalenessThreshold defines the threshold for considering data as stale.
+	// if data of a request hasn't been read/write in the last "stalenessThreshold", it is considered as stale data
+	// and will be cleaned in the next cleanup cycle.
+	stalenessThreshold = time.Minute * 5
+	// cleanupInterval defines the periodic interval that the cleanup go routine uses to check for stale data.
+	cleanupInterval = time.Minute
+)
+
+// NewPluginState initializes a new PluginState and returns its pointer.
+func NewPluginState(ctx context.Context) *PluginState {
+	pluginState := &PluginState{}
+	go pluginState.cleanup(ctx)
+	return pluginState
+}
+
+// PluginState provides a mechanism for plugins to store and retrieve arbitrary data by multiple extension points.
+// Data stored by the plugin in one extension point can be written, read or altered by another extension point.
+// The data stored in PluginState is always stored in the context of a given request.
+// If the data hasn't been accessed during "stalenessThreshold", it is cleaned by a cleanup internal mechanism.
+//
+// Note: PluginState uses a sync.Map to back the storage, because it is thread safe.
+// It's aimed to optimize for the "write once and read many times" scenarios.
+type PluginState struct {
+	// key: RequestID, value: map[StateKey]StateData
+	storage sync.Map
+	// key: RequestID, value: time.Time
+	requestToLastAccessTime sync.Map
+}
+
+// Read retrieves data with the given "key" in the context of "requestID" from PluginState.
+// If the key is not present, ErrNotFound is returned.
+func (s *PluginState) Read(requestID string, key StateKey) (StateData, error) {
+	s.requestToLastAccessTime.Store(requestID, time.Now())
+	stateMap, ok := s.storage.Load(requestID)
+	if !ok {
+		return nil, ErrNotFound
+	}
+
+	stateData := stateMap.(map[StateKey]StateData)
+	if value, ok := stateData[key]; ok {
+		return value, nil
+	}
+
+	return nil, ErrNotFound
+}
+
+// Write stores the given "val" in PluginState with the given "key" in the context of the given "requestID".
+func (s *PluginState) Write(requestID string, key StateKey, val StateData) {
+	s.requestToLastAccessTime.Store(requestID, time.Now())
+	var stateData map[StateKey]StateData
+	stateMap, ok := s.storage.Load(requestID)
+	if ok {
+		stateData = stateMap.(map[StateKey]StateData)
+	} else {
+		stateData = map[StateKey]StateData{}
+	}
+
+	stateData[key] = val
+
+	s.storage.Store(requestID, stateData)
+}
+
+// Delete deletes data associated with the given requestID.
+// It is possible to call Delete explicitly when the handling of a request is completed
+// or alternatively, if the request failed during its processing, a cleanup goroutine will
+// clean data of stale requests.
+func (s *PluginState) Delete(requestID string) {
+	s.storage.Delete(requestID)
+	s.requestToLastAccessTime.Delete(requestID)
+}
+
+// cleanup periodically deletes data associated with the given requestID.
+func (s *PluginState) cleanup(ctx context.Context) {
+	ticker := time.NewTicker(cleanupInterval)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			log.FromContext(ctx).V(logutil.DEFAULT).Info("Shutting down plugin state cleanup")
+			return
+		case <-ticker.C:
+			s.cleanStaleRequests()
+		}
+	}
+}
+
+// cleanStaleRequests iterates through all requests and removes those that haven't been
+// accessed for longer than stalenessThreshold. This operation is safe to run concurrently
+// with other operations on the PluginState.
+func (s *PluginState) cleanStaleRequests() {
+	s.requestToLastAccessTime.Range(func(k, v any) bool {
+		requestID := k.(string)
+		lastAccessTime := v.(time.Time)
+		if time.Since(lastAccessTime) > stalenessThreshold {
+			s.Delete(requestID) // cleanup stale requests (this is safe in sync.Map)
+		}
+		return true
+	})
+}
+
+// ReadPluginStateKey retrieves data with the given key from PluginState and asserts it to type T.
+// Returns an error if the key is not found or the type assertion fails.
+func ReadPluginStateKey[T StateData](state *PluginState, requestID string, key StateKey) (T, error) {
+	var zero T
+
+	raw, err := state.Read(requestID, key)
+	if err != nil {
+		return zero, err
+	}
+
+	val, ok := raw.(T)
+	if !ok {
+		return zero, fmt.Errorf("unexpected type for key %q: got %T", key, raw)
+	}
+
+	return val, nil
+}
diff --git a/pkg/epp/plugins/shared_state.go b/pkg/epp/plugins/shared_state.go
new file mode 100644
index 0000000..554e00f
--- /dev/null
+++ b/pkg/epp/plugins/shared_state.go
@@ -0,0 +1,35 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plugins
+
+import (
+	"errors"
+)
+
+var (
+	// ErrNotFound is the not found error message.
+	ErrNotFound = errors.New("not found")
+)
+
+// StateKey is the type of keys stored in PluginState.
+type StateKey string
+
+// StateData is a generic type for arbitrary data stored in PluginState.
+type StateData interface {
+	// Clone is an interface to make a copy of StateData.
+	Clone() StateData
+}
diff --git a/pkg/epp/requestcontrol/director.go b/pkg/epp/requestcontrol/director.go
index 3ba2818..1435c01 100644
--- a/pkg/epp/requestcontrol/director.go
+++ b/pkg/epp/requestcontrol/director.go
@@ -27,7 +27,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/go-logr/logr"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
@@ -71,6 +70,10 @@ type Director struct {
 	saturationDetector  SaturationDetector
 	preRequestPlugins   []PreRequest
 	postResponsePlugins []PostResponse
+	// we just need a pointer to an int variable since priority is a pointer in InferenceObjective
+	// no need to set this in the constructor, since the value we want is the default int val
+	// and value types cannot be nil
+	defaultPriority int
 }
 
 // HandleRequest orchestrates the request lifecycle:
@@ -91,7 +94,12 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	if !ok {
 		return reqCtx, errutil.Error{Code: errutil.BadRequest, Msg: "model not found in request body"}
 	}
-	reqCtx.TargetModelName = reqCtx.IncomingModelName // Default to incoming model name
+	if reqCtx.TargetModelName == "" {
+		// Default to incoming model name
+		reqCtx.TargetModelName = reqCtx.IncomingModelName
+	}
+	reqCtx.Request.Body["model"] = reqCtx.TargetModelName
+
 	prompt, err := requtil.ExtractPromptFromRequestBody(requestBodyMap)
 	if err != nil {
 		return reqCtx, err
@@ -99,26 +107,14 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	infObjective := d.datastore.ObjectiveGet(reqCtx.ObjectiveKey)
 	if infObjective == nil {
 		logger.V(logutil.VERBOSE).Info("No associated InferenceObjective found, using default", "objectiveKey", reqCtx.ObjectiveKey)
-		standard := v1alpha2.Standard
 		infObjective = &v1alpha2.InferenceObjective{
 			Spec: v1alpha2.InferenceObjectiveSpec{
-				Criticality: &standard,
+				Priority: &d.defaultPriority,
 			},
 		}
-	}
-
-	if infObjective.Spec.Criticality == nil {
-		// Default to Standard if not specified.
-		standard := v1alpha2.Standard
-		infObjective.Spec.Criticality = &standard
-	}
-
-	if len(infObjective.Spec.TargetModels) > 0 {
-		reqCtx.TargetModelName = RandomWeightedDraw(logger, infObjective, 0)
-		if reqCtx.TargetModelName == "" {
-			return reqCtx, errutil.Error{Code: errutil.BadConfiguration, Msg: fmt.Sprintf("error getting target model name for model %v", infObjective.Name)}
-		}
-		reqCtx.Request.Body["model"] = reqCtx.TargetModelName // Update target model in the body.
+	} else if infObjective.Spec.Priority == nil {
+		// Default to 0 if not specified.
+		infObjective.Spec.Priority = &d.defaultPriority
 	}
 
 	// Prepare LLMRequest (needed for both saturation detection and Scheduler)
@@ -129,13 +125,13 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 		Headers:     reqCtx.Request.Headers,
 	}
 
-	logger = logger.WithValues("objectiveKey", reqCtx.ObjectiveKey, "incomingModelName", reqCtx.IncomingModelName, "targetModelName", reqCtx.TargetModelName, "criticality", infObjective.Spec.Criticality)
+	logger = logger.WithValues("objectiveKey", reqCtx.ObjectiveKey, "incomingModelName", reqCtx.IncomingModelName, "targetModelName", reqCtx.TargetModelName, "priority", infObjective.Spec.Priority)
 
 	ctx = log.IntoContext(ctx, logger)
 	logger.V(logutil.DEBUG).Info("LLM request assembled")
 
 	// --- 2. Admission Control check --
-	if err := d.admitRequest(ctx, *infObjective.Spec.Criticality, reqCtx.FairnessID); err != nil {
+	if err := d.admitRequest(ctx, *infObjective.Spec.Priority, reqCtx.FairnessID); err != nil {
 		return reqCtx, err
 	}
 
@@ -161,22 +157,24 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 }
 
 // admitRequest handles admission control to decide whether or not to accept the request
-// based on the request criticality and system saturation state.
-func (d *Director) admitRequest(ctx context.Context, requestCriticality v1alpha2.Criticality, fairnessID string) error {
+// based on the request priority and system saturation state.
+func (d *Director) admitRequest(ctx context.Context, requestPriority int, fairnessID string) error {
 	logger := log.FromContext(ctx)
 
-	logger.V(logutil.TRACE).Info("Entering Flow Control", "criticality", requestCriticality, "fairnessID", fairnessID)
+	logger.V(logutil.TRACE).Info("Entering Flow Control", "priority", requestPriority, "fairnessID", fairnessID)
 
-	if requestCriticality == v1alpha2.Critical {
-		logger.V(logutil.DEBUG).Info("Critical request bypassing saturation check.")
+	// 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 {
+		logger.V(logutil.TRACE).Info("Non-sheddable request bypassing saturation check.")
 		return nil
 	}
 
-	logger.V(logutil.DEBUG).Info("Performing saturation check for non-critical request.")
 	if d.saturationDetector.IsSaturated(ctx) { // Assuming non-nil Saturation Detector
 		return errutil.Error{
 			Code: errutil.InferencePoolResourceExhausted,
-			Msg:  "system saturated, non-critical request dropped",
+			Msg:  "system saturated, sheddable request dropped",
 		}
 	}
 
@@ -194,13 +192,13 @@ func (d *Director) getCandidatePodsForScheduling(ctx context.Context, requestMet
 
 	subsetMap, found := requestMetadata[metadata.SubsetFilterNamespace].(map[string]any)
 	if !found {
-		return d.toSchedulerPodMetrics(d.datastore.PodList(backendmetrics.AllPodPredicate))
+		return d.toSchedulerPodMetrics(d.datastore.PodList(backendmetrics.AllPodsPredicate))
 	}
 
 	// Check if endpoint key is present in the subset map and ensure there is at least one value
 	endpointSubsetList, found := subsetMap[metadata.SubsetFilterKey].([]any)
 	if !found {
-		return d.toSchedulerPodMetrics(d.datastore.PodList(backendmetrics.AllPodPredicate))
+		return d.toSchedulerPodMetrics(d.datastore.PodList(backendmetrics.AllPodsPredicate))
 	} else if len(endpointSubsetList) == 0 {
 		loggerTrace.Info("found empty subset filter in request metadata, filtering all pods")
 		return []schedulingtypes.Pod{}
@@ -242,7 +240,10 @@ func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestC
 		return reqCtx, err
 	}
 	targetPods := []*backend.Pod{}
-	targetPort := int(pool.Spec.TargetPortNumber)
+	if len(pool.Spec.TargetPorts) != 1 {
+		return reqCtx, errutil.Error{Code: errutil.BadRequest, Msg: "targetPorts should have length 1"}
+	}
+	targetPort := int(pool.Spec.TargetPorts[0].Number)
 	targetEndpoints := []string{}
 
 	for _, pod := range result.ProfileResults[result.PrimaryProfileName].TargetPods {
@@ -286,7 +287,7 @@ func (d *Director) HandleResponse(ctx context.Context, reqCtx *handlers.RequestC
 }
 
 func (d *Director) GetRandomPod() *backend.Pod {
-	pods := d.datastore.PodList(backendmetrics.AllPodPredicate)
+	pods := d.datastore.PodList(backendmetrics.AllPodsPredicate)
 	if len(pods) == 0 {
 		return nil
 	}
@@ -295,37 +296,6 @@ func (d *Director) GetRandomPod() *backend.Pod {
 	return pod.GetPod()
 }
 
-func RandomWeightedDraw(logger logr.Logger, model *v1alpha2.InferenceObjective, seed int64) string {
-	// TODO: after we are down to 1 server implementation, make these methods a part of the struct
-	// and handle random seeding on the struct.
-	source := rand.NewSource(rand.Int63())
-	if seed > 0 {
-		source = rand.NewSource(seed)
-	}
-	r := rand.New(source)
-
-	// all the weight values are nil, then we should return random model name
-	if model.Spec.TargetModels[0].Weight == nil {
-		index := r.Int31n(int32(len(model.Spec.TargetModels)))
-		return model.Spec.TargetModels[index].Name
-	}
-
-	var weights int32
-	for _, model := range model.Spec.TargetModels {
-		weights += *model.Weight
-	}
-	logger.V(logutil.TRACE).Info("Weights for model computed", "model", model.Name, "weights", weights)
-	randomVal := r.Int31n(weights)
-	// TODO: optimize this without using loop
-	for _, model := range model.Spec.TargetModels {
-		if randomVal < *model.Weight {
-			return model.Name
-		}
-		randomVal -= *model.Weight
-	}
-	return ""
-}
-
 func (d *Director) runPreRequestPlugins(ctx context.Context, request *schedulingtypes.LLMRequest,
 	schedulingResult *schedulingtypes.SchedulingResult, targetPort int) {
 	loggerDebug := log.FromContext(ctx).V(logutil.DEBUG)
diff --git a/pkg/epp/saturationdetector/saturationdetector.go b/pkg/epp/saturationdetector/saturationdetector.go
index 57be1af..bc47ecd 100644
--- a/pkg/epp/saturationdetector/saturationdetector.go
+++ b/pkg/epp/saturationdetector/saturationdetector.go
@@ -109,7 +109,7 @@ func NewDetector(config *Config, datastore Datastore, logger logr.Logger) *Detec
 func (d *Detector) IsSaturated(ctx context.Context) bool {
 	logger := log.FromContext(ctx).WithName(loggerName)
 	// TODO: filter out stale metrics here if needed.
-	allPodsMetrics := d.datastore.PodList(backendmetrics.AllPodPredicate)
+	allPodsMetrics := d.datastore.PodList(backendmetrics.AllPodsPredicate)
 	if len(allPodsMetrics) == 0 {
 		logger.V(logutil.VERBOSE).Info("No pods found in datastore; system is considered SATURATED (no capacity).")
 		// If there are no pods, there is no capacity to serve requests.
diff --git a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
index 381ec97..9c1a8d8 100644
--- a/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
+++ b/pkg/epp/scheduling/framework/plugins/multi/prefix/plugin.go
@@ -96,7 +96,7 @@ func (s ServerID) String() string {
 }
 
 // compile-time type validation
-var _ types.StateData = &SchedulingContextState{}
+var _ plugins.StateData = &SchedulingContextState{}
 
 // SchedulingContextState is the state of this plugin to be used during a scheduling cycle.
 type SchedulingContextState struct {
@@ -106,7 +106,7 @@ type SchedulingContextState struct {
 	PrefixCacheServers map[ServerID]int
 }
 
-func (s *SchedulingContextState) Clone() types.StateData {
+func (s *SchedulingContextState) Clone() plugins.StateData {
 	prefixHashes := make([]BlockHash, len(s.PrefixHashes))
 	copy(prefixHashes, s.PrefixHashes)
 	prefixCacheServers := make(map[ServerID]int, len(s.PrefixCacheServers))
@@ -180,7 +180,7 @@ func (m *Plugin) Score(ctx context.Context, cycleState *types.CycleState, reques
 		PrefixCacheServers: m.matchLongestPrefix(ctx, hashes),
 	}
 
-	cycleState.Write(types.StateKey(m.TypedName().Type), state)
+	cycleState.Write(plugins.StateKey(m.TypedName().Type), state)
 	loggerTrace.Info(fmt.Sprintf("cached servers: %+v", state.PrefixCacheServers), "hashes", state.PrefixHashes)
 	// calculate the scores of pods
 	scores := make(map[types.Pod]float64, len(pods))
diff --git a/pkg/epp/scheduling/types/cycle_state.go b/pkg/epp/scheduling/types/cycle_state.go
index 40ecfa4..4ac617a 100644
--- a/pkg/epp/scheduling/types/cycle_state.go
+++ b/pkg/epp/scheduling/types/cycle_state.go
@@ -17,25 +17,12 @@ limitations under the License.
 package types
 
 import (
-	"errors"
 	"fmt"
 	"sync"
-)
 
-var (
-	// ErrNotFound is the not found error message.
-	ErrNotFound = errors.New("not found")
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 )
 
-// StateData is a generic type for arbitrary data stored in CycleState.
-type StateData interface {
-	// Clone is an interface to make a copy of StateData.
-	Clone() StateData
-}
-
-// StateKey is the type of keys stored in CycleState.
-type StateKey string
-
 // NewCycleState initializes a new CycleState and returns its pointer.
 func NewCycleState() *CycleState {
 	return &CycleState{}
@@ -55,30 +42,30 @@ type CycleState struct {
 // present, ErrNotFound is returned.
 //
 // See CycleState for notes on concurrency.
-func (c *CycleState) Read(key StateKey) (StateData, error) {
+func (c *CycleState) Read(key plugins.StateKey) (plugins.StateData, error) {
 	if v, ok := c.storage.Load(key); ok {
-		return v.(StateData), nil
+		return v.(plugins.StateData), nil
 	}
-	return nil, ErrNotFound
+	return nil, plugins.ErrNotFound
 }
 
 // Write stores the given "val" in CycleState with the given "key".
 //
 // See CycleState for notes on concurrency.
-func (c *CycleState) Write(key StateKey, val StateData) {
+func (c *CycleState) Write(key plugins.StateKey, val plugins.StateData) {
 	c.storage.Store(key, val)
 }
 
 // Delete deletes data with the given key from CycleState.
 //
 // See CycleState for notes on concurrency.
-func (c *CycleState) Delete(key StateKey) {
+func (c *CycleState) Delete(key plugins.StateKey) {
 	c.storage.Delete(key)
 }
 
 // ReadCycleStateKey  retrieves data with the given key from CycleState and asserts it to type T.
 // Returns an error if the key is not found or the type assertion fails.
-func ReadCycleStateKey[T StateData](c *CycleState, key StateKey) (T, error) {
+func ReadCycleStateKey[T plugins.StateData](c *CycleState, key plugins.StateKey) (T, error) {
 	var zero T
 
 	raw, err := c.Read(key)
diff --git a/pkg/epp/server/controller_manager.go b/pkg/epp/server/controller_manager.go
index 05038f7..47e4f12 100644
--- a/pkg/epp/server/controller_manager.go
+++ b/pkg/epp/server/controller_manager.go
@@ -85,12 +85,23 @@ func defaultManagerOptions(gknn common.GKNN, metricsServerOptions metricsserver.
 }
 
 // NewDefaultManager creates a new controller manager with default configuration.
-func NewDefaultManager(gknn common.GKNN, restConfig *rest.Config, metricsServerOptions metricsserver.Options) (ctrl.Manager, error) {
+func NewDefaultManager(gknn common.GKNN, restConfig *rest.Config, metricsServerOptions metricsserver.Options, leaderElectionEnabled bool) (ctrl.Manager, error) {
 	opt, err := defaultManagerOptions(gknn, metricsServerOptions)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create controller manager options: %v", err)
 	}
+
+	if leaderElectionEnabled {
+		opt.LeaderElection = true
+		opt.LeaderElectionResourceLock = "leases"
+		// The lease name needs to be unique per EPP deployment.
+		opt.LeaderElectionID = fmt.Sprintf("epp-%s-%s.gateway-api-inference-extension.sigs.k8s.io", gknn.Namespace, gknn.Name)
+		opt.LeaderElectionNamespace = gknn.Namespace
+		opt.LeaderElectionReleaseOnCancel = true
+	}
+
 	manager, err := ctrl.NewManager(restConfig, opt)
+
 	if err != nil {
 		return nil, fmt.Errorf("failed to create controller manager: %v", err)
 	}
diff --git a/site-src/api-types/inferencepool.md b/site-src/api-types/inferencepool.md
index d279447..c4481b1 100644
--- a/site-src/api-types/inferencepool.md
+++ b/site-src/api-types/inferencepool.md
@@ -33,7 +33,8 @@ kind: InferencePool
 metadata:
   name: vllm-llama3-8b-instruct
 spec:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   selector:
     app: vllm-llama3-8b-instruct
   extensionRef:
diff --git a/site-src/contributing/devguide.md b/site-src/contributing/devguide.md
new file mode 100644
index 0000000..9e45a0d
--- /dev/null
+++ b/site-src/contributing/devguide.md
@@ -0,0 +1,51 @@
+# Developer Guide
+
+## Integration Tests Debug Guide
+This document provides detailed instructions on how to run and debug integration tests locally in debug mode.
+
+### Prerequisites
+
+#### 1. Install Required Tools
+Ensure the envtest tool is installed:
+```bash
+$ make envtest
+```
+
+#### 2. Verify Kubernetes Test Environment
+Run the following command to set up and verify the test environment:
+```bash
+$ ./bin/setup-envtest use 1.31.0 --bin-dir ./bin -p path
+bin/k8s/1.31.0-darwin-arm64
+```
+### Run test in shell
+```shell
+export KUBEBUILDER_ASSETS=<project-root-path>/bin/k8s/1.31.0-<platform-architecture> 
+go test sigs.k8s.io/gateway-api-inference-extension/test/integration/epp -run <test-function-name>
+```
+
+### Configure and Run in GoLand
+
+#### 1. Create Test Configuration
+Select the test case you want to debug:
+![](../images/modify-run-configuration.png)
+
+#### 2. Configure Environment Variables
+Set environment variables in the Run/Debug Configuration:
+
+![](../images/edit-environment-variables.png)
+
+**Required environment variable:**
+
+- **Name:** `KUBEBUILDER_ASSETS`
+- **Value:** `<project-root-path>/bin/k8s/1.31.0-<platform-architecture>`
+
+**Example path:**
+```
+/go/src/kubernetes.io/gateway-api-inference-extension/bin/k8s/1.31.0-darwin-arm64
+```
+
+#### 3. Set Breakpoints and Run
+
+Example Output:
+
+![](../images/running-example.png)
diff --git a/site-src/guides/adapter-rollout.md b/site-src/guides/adapter-rollout.md
index 0325ce2..0936d29 100644
--- a/site-src/guides/adapter-rollout.md
+++ b/site-src/guides/adapter-rollout.md
@@ -68,7 +68,7 @@ kind: InferenceModel
 metadata:
   name: food-review
 spec:
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: vllm-llama3-8b-instruct
   targetModels:
@@ -106,7 +106,7 @@ kind: InferenceModel
 metadata:
   name: food-review
 spec:
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: vllm-llama3-8b-instruct
   targetModels:
diff --git a/site-src/guides/implementers.md b/site-src/guides/implementers.md
index 4fb6ee7..6fce016 100644
--- a/site-src/guides/implementers.md
+++ b/site-src/guides/implementers.md
@@ -47,7 +47,8 @@ kind: InferencePool
 metadata:
   name: vllm-llama3-8b-instruct
 spec:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   selector:
     app: vllm-llama3-8b-instruct
   extensionRef:
@@ -141,7 +142,7 @@ Supporting this broad range of extension capabilities (including for inference,
 Several implementations can be used as references:
 
 - A fully featured [reference implementation](https://github.com/envoyproxy/envoy/tree/main/source/extensions/filters/http/ext_proc) (C++) can be found in the Envoy GitHub repository.
-- A second implementation (Rust, non-Envoy) is available in [Agent Gateway](https://github.com/agentgateway/agentgateway/blob/v0.5.2/crates/proxy/src/ext_proc.rs).
+- A second implementation (Rust, non-Envoy) is available in [agentgateway](https://github.com/agentgateway/agentgateway/blob/v0.7.2/crates/agentgateway/src/http/ext_proc.rs).
 
 #### Portable Implementation
 
diff --git a/site-src/guides/index.md b/site-src/guides/index.md
index 0cf6437..e02ffb5 100644
--- a/site-src/guides/index.md
+++ b/site-src/guides/index.md
@@ -71,30 +71,22 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
 
 ### Install the Inference Extension CRDs
 
-=== "Latest Release"
-
-      ```bash
-      kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/releases/latest/download/manifests.yaml
-      ```
-
-=== "Dev Version"
-
-      ```bash
-      kubectl apply -k https://github.com/kubernetes-sigs/gateway-api-inference-extension/config/crd
-      ```
+   ```bash
+   kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/releases/latest/download/manifests.yaml
+   ```
 
 ### Deploy InferenceModel
 
    Deploy the sample InferenceModel which is configured to forward traffic to the `food-review-1` [LoRA adapter](https://docs.vllm.ai/en/latest/features/lora.html) of the sample model server.
 
    ```bash
-   kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/main/config/manifests/inferenceobjective.yaml
+   kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api-inference-extension/refs/tags/v0.5.1/config/manifests/inferencemodel.yaml
    ```
 
 ### Deploy the InferencePool and Endpoint Picker Extension
 
    ```bash
-   kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/main/config/manifests/inferencepool-resources.yaml
+   kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api-inference-extension/refs/tags/v0.5.1/config/manifests/inferencepool-resources.yaml
    ```
 
 ### Deploy an Inference Gateway
@@ -161,7 +153,7 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
          wget https://storage.googleapis.com/istio-build/dev/$TAG/istioctl-$TAG-win.zip
          unzip istioctl-$TAG-win.zip
 
-         ./istioctl install --set tag=$TAG --set hub=gcr.io/istio-testing
+         ./istioctl install --set tag=$TAG --set hub=gcr.io/istio-testing --set values.pilot.env.ENABLE_GATEWAY_API_INFERENCE_EXTENSION=true
          ```
 
       3. If you run the Endpoint Picker (EPP) with the `--secure-serving` flag set to `true` (the default mode), it is currently using a self-signed certificate. As a security measure, Istio does not trust self-signed certificates by default. As a temporary workaround, you can apply the destination rule to bypass TLS verification for EPP. A more secure TLS implementation in EPP is being discussed in [Issue 582](https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/582).
@@ -244,6 +236,53 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
          kubectl get httproute llm-route -o yaml
          ```
 
+=== "Agentgateway"
+
+      [Agentgateway](https://agentgateway.dev/) is a purpose-built proxy designed for AI workloads, and comes with native support for inference routing. Agentgateway integrates with [Kgateway](https://kgateway.dev/) as it's control plane.
+
+      1. Requirements
+
+         - [Helm](https://helm.sh/docs/intro/install/) installed.
+         - Gateway API [CRDs](https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api) installed.
+
+      2. Set the Kgateway version and install the Kgateway CRDs.
+
+         ```bash
+         KGTW_VERSION=v2.0.4
+         helm upgrade -i --create-namespace --namespace kgateway-system --version $KGTW_VERSION kgateway-crds oci://cr.kgateway.dev/kgateway-dev/charts/kgateway-crds
+         ```
+
+      3. Install Kgateway
+
+         ```bash
+         helm upgrade -i --namespace kgateway-system --version $KGTW_VERSION kgateway oci://cr.kgateway.dev/kgateway-dev/charts/kgateway --set inferenceExtension.enabled=true --set agentGateway.enabled=true
+         ```
+
+      4. Deploy the Gateway
+
+         ```bash
+         kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/main/config/manifests/gateway/agentgateway/gateway.yaml
+         ```
+
+         Confirm that the Gateway was assigned an IP address and reports a `Programmed=True` status:
+         ```bash
+         $ kubectl get gateway inference-gateway
+         NAME                CLASS               ADDRESS         PROGRAMMED   AGE
+         inference-gateway   agentgateway        <MY_ADDRESS>    True         22s
+         ```
+
+      5. Deploy the HTTPRoute
+
+         ```bash
+         kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/main/config/manifests/gateway/agentgateway/httproute.yaml
+         ```
+
+      6. Confirm that the HTTPRoute status conditions include `Accepted=True` and `ResolvedRefs=True`:
+
+         ```bash
+         kubectl get httproute llm-route -o yaml
+         ```
+
 ### Try it out
 
    Wait until the gateway is ready.
@@ -339,3 +378,25 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
          ```bash
          kubectl delete ns kgateway-system
          ```
+
+=== "Agentgateway"
+
+      The following instructions assume you would like to cleanup ALL Kgateway resources that were created in this quickstart guide.
+
+      1. Uninstall Kgateway
+
+         ```bash
+         helm uninstall kgateway -n kgateway-system
+         ```
+
+      1. Uninstall the Kgateway CRDs.
+
+         ```bash
+         helm uninstall kgateway-crds -n kgateway-system
+         ```
+
+      1. Remove the Kgateway namespace.
+
+         ```bash
+         kubectl delete ns kgateway-system
+         ```
diff --git a/site-src/guides/inferencepool-rollout.md b/site-src/guides/inferencepool-rollout.md
index f5bef65..98b3cc4 100644
--- a/site-src/guides/inferencepool-rollout.md
+++ b/site-src/guides/inferencepool-rollout.md
@@ -56,7 +56,7 @@ kind: InferenceModel
 metadata:
   name: food-review-new
 spec:
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: vllm-llama3-8b-instruct-new
   targetModels:
@@ -208,7 +208,8 @@ kind: InferencePool
 metadata:
   name: vllm-llama3-8b-instruct-new
 spec:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   selector:
     app: vllm-llama3-8b-instruct-new
   extensionRef:
diff --git a/site-src/guides/metrics-and-observability.md b/site-src/guides/metrics-and-observability.md
index 07b86a0..34b0db1 100644
--- a/site-src/guides/metrics-and-observability.md
+++ b/site-src/guides/metrics-and-observability.md
@@ -126,6 +126,81 @@ PROFILE_NAME=heap
 curl -H "Authorization: Bearer $TOKEN" localhost:9090/debug/pprof/$PROFILE_NAME -o profile.out
 go tool pprof -png profile.out
 ```
+## Setting Up Grafana + Prometheus
+
+### Grafana
+
+A simple grafana deployment can be done with the following commands:
+
+```bash
+helm repo add grafana https://grafana.github.io/helm-charts
+helm install grafana grafana/grafana --namespace monitoring --create-namespace
+```
+
+Get the Grafana URL to visit by running these commands in the same shell:
+
+```bash
+kubectl -n monitoring port-forward deploy/grafana 3000:3000
+```
+
+Get the generated password for the `admin` user:
+
+```bash
+kubectl -n monitoring get secret grafana \
+  -o go-template='{% raw %}{{ index .data "admin-password" | base64decode }}{% endraw %}'
+```
+
+You can now access the Grafana UI from [http://127.0.0.1:3000](http://127.0.0.1:3000)
+
+### Prometheus
+
+We currently have 2 types of prometheus deployments documented:
+
+1. Self Hosted using the prometheus helm chart
+2. Using Google Managed Prometheus
+
+=== "Self-Hosted"
+
+    Create necessary ServiceAccount and RBAC resources:
+
+     ```bash
+        kubectl apply -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/main/config/observability/prometheus/rbac.yaml
+     ```
+
+    Patch the metrics reader ClusterRoleBinding to reference the new ServiceAccount:
+    ```bash
+       kubectl patch clusterrolebinding inference-gateway-sa-metrics-reader-role-binding \
+         --type='json' \
+         -p='[{"op": "replace", "path": "/subjects/0/namespace", "value": "monitoring"}]'
+    ```
+
+    Add the prometheus-community helm repository:
+
+     ```bash
+        helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+     ```
+
+    Deploy the prometheus helm chart using this command:
+     ```bash
+        helm install prometheus prometheus-community/prometheus \
+         --namespace monitoring \
+         -f https://github.com/kubernetes-sigs/gateway-api-inference-extension/raw/main/config/observability/prometheus/values.yaml
+     ```
+
+    You can add the prometheus data source to grafana following [This Guide](https://grafana.com/docs/grafana/latest/administration/data-source-management/).
+    The prometheus server host is by default `http://prometheus-server`
+
+    Notice that the given values file is very simple and will work directly after following the [Getting Started Guide](https://gateway-api-inference-extension.sigs.k8s.io/guides/), you might need to modify it
+
+=== "Google Managed"
+
+    If you run the inference gateway with [Google Managed Prometheus](https://cloud.google.com/stackdriver/docs/managed-prometheus), please follow the [instructions](https://cloud.google.com/stackdriver/docs/managed-prometheus/query)
+    to configure Google Managed Prometheus as data source for the grafana dashboard.
+
+## Load Inference Extension dashboard into Grafana
+
+Please follow [grafana instructions](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/import-dashboards/) to load the dashboard json.
+The dashboard can be found here [Grafana Dashboard](https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/tools/dashboards/inference_gateway.json)
 
 ## Prometheus Alerts
 
diff --git a/site-src/guides/serve-multiple-lora-adapters.md b/site-src/guides/serve-multiple-lora-adapters.md
index af78d26..59cfe72 100644
--- a/site-src/guides/serve-multiple-lora-adapters.md
+++ b/site-src/guides/serve-multiple-lora-adapters.md
@@ -27,7 +27,7 @@ kind: InferenceModel
 metadata:
   name: english-bot
 spec:
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: gemma3
         
@@ -37,7 +37,7 @@ kind: InferenceModel
 metadata:
   name: spanish-bot
 spec:
-  criticality: Critical
+  criticality: 2
   poolRef:
     name: gemma3
     
diff --git a/site-src/images/edit-environment-variables.png b/site-src/images/edit-environment-variables.png
new file mode 100644
index 0000000..bf2030c
Binary files /dev/null and b/site-src/images/edit-environment-variables.png differ
diff --git a/site-src/images/modify-run-configuration.png b/site-src/images/modify-run-configuration.png
new file mode 100644
index 0000000..d72b81c
Binary files /dev/null and b/site-src/images/modify-run-configuration.png differ
diff --git a/site-src/images/running-example.png b/site-src/images/running-example.png
new file mode 100644
index 0000000..f909ee2
Binary files /dev/null and b/site-src/images/running-example.png differ
diff --git a/site-src/implementations/gateways.md b/site-src/implementations/gateways.md
index 53e6f92..5ab1e09 100644
--- a/site-src/implementations/gateways.md
+++ b/site-src/implementations/gateways.md
@@ -2,17 +2,45 @@
 
 This project has several implementations that are planned or in progress:
 
-* [Envoy AI Gateway][1]
-* [Kgateway][2]
-* [Google Kubernetes Engine][3]
-* [Istio][4]
-* [Alibaba Cloud Container Service for Kubernetes][5]
-
-[1]:#envoy-gateway
-[2]:#kgateway
-[3]:#google-kubernetes-engine
-[4]:#istio
-[5]:#alibaba-cloud-container-service-for-kubernetes
+* [Agentgateway][1]
+* [Alibaba Cloud Container Service for Kubernetes][2]
+* [Envoy AI Gateway][3]
+* [Google Kubernetes Engine][4]
+* [Istio][5]
+* [Kgateway][6]
+
+[1]:#agentgateway
+[2]:#alibaba-cloud-container-service-for-kubernetes
+[3]:#envoy-ai-gateway
+[4]:#google-kubernetes-engine
+[5]:#istio
+[6]:#kgateway
+
+## Agentgateway
+
+[Agentgateway](https://agentgateway.dev/) is an open source Gateway API implementation focusing on AI use cases, including LLM consumption, LLM serving, agent-to-agent ([A2A](https://a2aproject.github.io/A2A/latest/)), and agent-to-tool ([MCP](https://modelcontextprotocol.io/introduction)). It is the first and only proxy designed specifically for the Kubernetes Gateway API, powered by a high performance and scalable Rust dataplane implementation.
+
+Agentgateway comes with native support for Gateway API Inference Extension, powered by the [Kgateway](https://kgateway.dev/) control plane.
+
+## Alibaba Cloud Container Service for Kubernetes
+
+[Alibaba Cloud Container Service for Kubernetes (ACK)][ack] is a managed Kubernetes platform 
+offered by Alibaba Cloud. The implementation of the Gateway API in ACK is through the 
+[ACK Gateway with Inference Extension][ack-gie] component, which introduces model-aware, 
+GPU-efficient load balancing for AI workloads beyond basic HTTP routing.
+
+The ACK Gateway with Inference Extension implements the Gateway API Inference Extension 
+and provides optimized routing for serving generative AI workloads, 
+including weighted traffic splitting, mirroring, advanced routing, etc. 
+See the docs for the [usage][ack-gie-usage].
+
+Progress towards supporting Gateway API Inference Extension is being tracked 
+by [this Issue](https://github.com/AliyunContainerService/ack-gateway-api/issues/1).
+
+[ack]:https://www.alibabacloud.com/help/en/ack
+[ack-gie]:https://www.alibabacloud.com/help/en/ack/product-overview/ack-gateway-with-inference-extension
+[ack-gie-usage]:https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/intelligent-routing-and-traffic-management-with-ack-gateway-inference-extension
+
 
 ## Envoy AI Gateway
 
@@ -29,14 +57,6 @@ Issue](https://github.com/envoyproxy/ai-gateway/issues/423).
 [aigw-capabilities]:https://aigateway.envoyproxy.io/docs/capabilities/
 [aigw-quickstart]:https://aigateway.envoyproxy.io/docs/capabilities/gateway-api-inference-extension
 
-## Kgateway
-
-[Kgateway](https://kgateway.dev/) is a Gateway API Inference Extension
-[conformant](https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/conformance/reports/v0.5.1/gateway/kgateway)
-gateway 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.
-
 ## Google Kubernetes Engine
 
 [Google Kubernetes Engine (GKE)][gke] is a managed Kubernetes platform offered
@@ -66,21 +86,10 @@ For service mesh users, Istio also fully supports east-west (including [GAMMA](h
 Gateway API Inference Extension support is being tracked by this [GitHub
 Issue](https://github.com/istio/istio/issues/55768).
 
-## Alibaba Cloud Container Service for Kubernetes
-
-[Alibaba Cloud Container Service for Kubernetes (ACK)][ack] is a managed Kubernetes platform 
-offered by Alibaba Cloud. The implementation of the Gateway API in ACK is through the 
-[ACK Gateway with Inference Extension][ack-gie] component, which introduces model-aware, 
-GPU-efficient load balancing for AI workloads beyond basic HTTP routing.
-
-The ACK Gateway with Inference Extension implements the Gateway API Inference Extension 
-and provides optimized routing for serving generative AI workloads, 
-including weighted traffic splitting, mirroring, advanced routing, etc. 
-See the docs for the [usage][ack-gie-usage].
-
-Progress towards supporting Gateway API Inference Extension is being tracked 
-by [this Issue](https://github.com/AliyunContainerService/ack-gateway-api/issues/1).
+## Kgateway
 
-[ack]:https://www.alibabacloud.com/help/en/ack
-[ack-gie]:https://www.alibabacloud.com/help/en/ack/product-overview/ack-gateway-with-inference-extension
-[ack-gie-usage]:https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/intelligent-routing-and-traffic-management-with-ack-gateway-inference-extension
\ No newline at end of file
+[Kgateway](https://kgateway.dev/) is a Gateway API Inference Extension
+[conformant](https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/conformance/reports/v0.5.1/gateway/kgateway)
+gateway 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.
diff --git a/site-src/implementations/model-servers.md b/site-src/implementations/model-servers.md
index 72f9931..da9968f 100644
--- a/site-src/implementations/model-servers.md
+++ b/site-src/implementations/model-servers.md
@@ -1,5 +1,3 @@
-
-
 # Supported Model Servers
 
 Any model server that conform to the [model server protocol](https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/docs/proposals/003-model-server-protocol) are supported by the inference extension.
@@ -11,6 +9,7 @@ Any model server that conform to the [model server protocol](https://github.com/
 | vLLM V0              | v0.6.4 and above                                                                                                       | [commit 0ad216f](https://github.com/vllm-project/vllm/commit/0ad216f5750742115c686723bf38698372d483fd)                            |                                                                                                             |
 | vLLM V1              | v0.8.0 and above                                                                                                       | [commit bc32bc7](https://github.com/vllm-project/vllm/commit/bc32bc73aad076849ac88565cff745b01b17d89c)                            |                                                                                                             |
 | Triton(TensorRT-LLM) | [25.03](https://docs.nvidia.com/deeplearning/triton-inference-server/release-notes/rel-25-03.html#rel-25-03) and above | [commit 15cb989](https://github.com/triton-inference-server/tensorrtllm_backend/commit/15cb989b00523d8e92dce5165b9b9846c047a70d). | LoRA affinity feature is not available as the required LoRA metrics haven't been implemented in Triton yet. [Feature request](https://github.com/triton-inference-server/server/issues/8181) |
+| SGLang               | v0.4.0 and above | [commit 1929c06](https://github.com/sgl-project/sglang/commit/1929c067625089c9c3c04321578f450275f24041) | Set `--enable-metrics` on the model server. LoRA affinity feature is not available as the required LoRA metrics haven't been implemented in SGLang yet.
 
 ## vLLM
 
@@ -36,3 +35,18 @@ Use `--set inferencePool.modelServerType=triton-tensorrt-llm` to install the [`i
 - --lora-info-metric
 - "" # Set an empty metric to disable LoRA metric scraping as they are not supported by Triton yet.
 ```
+
+## SGLang
+
+### Edit EPP deployment yaml
+
+ Add the following to the `args` of the [EPP deployment](https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/42eb5ff1c5af1275df43ac384df0ddf20da95134/config/manifests/inferencepool-resources.yaml#L32)
+
+```
+- --totalQueuedRequestsMetric
+- "sglang:num_queue_reqs"
+- --kvCacheUsagePercentageMetric
+- "sglang:token_usage"
+- --lora-info-metric
+- "" # Set an empty metric to disable LoRA metric scraping as they are not supported by SGLang yet.
+```



Test patch:
diff --git a/apix/v1alpha2/inferencepool_conversion_test.go b/apix/v1alpha2/inferencepool_conversion_test.go
new file mode 100644
index 0000000..847372b
--- /dev/null
+++ b/apix/v1alpha2/inferencepool_conversion_test.go
@@ -0,0 +1,390 @@
+/*
+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 v1alpha2
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
+)
+
+var (
+	group       = Group("my-group")
+	kind        = Kind("MyKind")
+	failureMode = ExtensionFailureMode("Deny")
+	portNumber  = PortNumber(9000)
+	timestamp   = metav1.Unix(0, 0)
+
+	v1Group       = v1.Group("my-group")
+	v1Kind        = v1.Kind("MyKind")
+	v1FailureMode = v1.ExtensionFailureMode("Deny")
+	v1PortNumber  = v1.PortNumber(9000)
+)
+
+func TestInferencePoolConvertTo(t *testing.T) {
+	tests := []struct {
+		name    string
+		src     *InferencePool
+		want    *v1.InferencePool
+		wantErr bool
+	}{
+		{
+			name: "full conversion from v1alpha2 to v1 including status",
+			src: &InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.x-k8s.io/v1alpha2",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: InferencePoolSpec{
+					Selector: map[LabelKey]LabelValue{
+						"app": "my-model-server",
+					},
+					TargetPortNumber: 8080,
+					ExtensionRef: Extension{
+						Group:       &group,
+						Kind:        &kind,
+						Name:        "my-epp-service",
+						PortNumber:  &portNumber,
+						FailureMode: &failureMode,
+					},
+				},
+				Status: InferencePoolStatus{
+					Parents: []PoolStatus{
+						{
+							GatewayRef: ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &v1.InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.x-k8s.io/v1alpha2",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: v1.InferencePoolSpec{
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"app": "my-model-server",
+						},
+					},
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8080))}},
+					ExtensionRef: v1.Extension{
+						Group:       &v1Group,
+						Kind:        v1Kind,
+						Name:        "my-epp-service",
+						PortNumber:  v1PortNumber,
+						FailureMode: v1FailureMode,
+					},
+				},
+				Status: v1.InferencePoolStatus{
+					Parents: []v1.PoolStatus{
+						{
+							GatewayRef: v1.ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1.InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(v1.InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "conversion from v1alpha2 to v1 with empty extensionRef",
+			src: &InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.x-k8s.io/v1alpha2",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: InferencePoolSpec{
+					Selector: map[LabelKey]LabelValue{
+						"app": "my-model-server",
+					},
+					TargetPortNumber: 8080,
+				},
+				Status: InferencePoolStatus{
+					Parents: []PoolStatus{
+						{
+							GatewayRef: ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &v1.InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.x-k8s.io/v1alpha2",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: v1.InferencePoolSpec{
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"app": "my-model-server",
+						},
+					},
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8080))}},
+				},
+				Status: v1.InferencePoolStatus{
+					Parents: []v1.PoolStatus{
+						{
+							GatewayRef: v1.ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1.InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(v1.InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := &v1.InferencePool{}
+			err := tt.src.ConvertTo(got)
+			if (err != nil) != tt.wantErr {
+				t.Fatalf("ConvertTo() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("ConvertTo() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestInferencePoolConvertFrom(t *testing.T) {
+	tests := []struct {
+		name    string
+		src     *v1.InferencePool
+		want    *InferencePool
+		wantErr bool
+	}{
+		{
+			name: "full conversion from v1 to v1alpha2 including status",
+			src: &v1.InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.k8s.io/v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: v1.InferencePoolSpec{
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"app": "my-model-server",
+						},
+					},
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8080))}},
+					ExtensionRef: v1.Extension{
+						Group:       &v1Group,
+						Kind:        v1Kind,
+						Name:        "my-epp-service",
+						PortNumber:  v1PortNumber,
+						FailureMode: v1FailureMode,
+					},
+				},
+				Status: v1.InferencePoolStatus{
+					Parents: []v1.PoolStatus{
+						{
+							GatewayRef: v1.ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1.InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(v1.InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.k8s.io/v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: InferencePoolSpec{
+					Selector: map[LabelKey]LabelValue{
+						"app": "my-model-server",
+					},
+					TargetPortNumber: 8080,
+					ExtensionRef: Extension{
+						Group:       &group,
+						Kind:        &kind,
+						Name:        "my-epp-service",
+						PortNumber:  &portNumber,
+						FailureMode: &failureMode,
+					},
+				},
+				Status: InferencePoolStatus{
+					Parents: []PoolStatus{
+						{
+							GatewayRef: ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "conversion from v1 to v1alpha2 with empty extensionRef",
+			src: &v1.InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.k8s.io/v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: v1.InferencePoolSpec{
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"app": "my-model-server",
+						},
+					},
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8080))}},
+				},
+				Status: v1.InferencePoolStatus{
+					Parents: []v1.PoolStatus{
+						{
+							GatewayRef: v1.ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(v1.InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(v1.InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &InferencePool{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "InferencePool",
+					APIVersion: "inference.networking.k8s.io/v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-pool",
+					Namespace: "test-ns",
+				},
+				Spec: InferencePoolSpec{
+					Selector: map[LabelKey]LabelValue{
+						"app": "my-model-server",
+					},
+					TargetPortNumber: 8080,
+				},
+				Status: InferencePoolStatus{
+					Parents: []PoolStatus{
+						{
+							GatewayRef: ParentGatewayReference{Name: "my-gateway"},
+							Conditions: []metav1.Condition{
+								{
+									Type:               string(InferencePoolConditionAccepted),
+									Status:             metav1.ConditionTrue,
+									Reason:             string(InferencePoolReasonAccepted),
+									LastTransitionTime: timestamp,
+								},
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:    "nil source",
+			src:     nil,
+			want:    &InferencePool{},
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := &InferencePool{}
+			err := got.ConvertFrom(tt.src)
+			if (err != nil) != tt.wantErr {
+				t.Fatalf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("ConvertFrom() mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/conformance/tests/epp_unavailable_fail_open.go b/conformance/tests/epp_unavailable_fail_open.go
index 36e0fa2..8af5972 100644
--- a/conformance/tests/epp_unavailable_fail_open.go
+++ b/conformance/tests/epp_unavailable_fail_open.go
@@ -29,7 +29,8 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/conformance/utils/config"
 	k8sutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/kubernetes"
 	trafficutils "sigs.k8s.io/gateway-api-inference-extension/conformance/utils/traffic"
-	testfilter "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test/filter"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test"
 )
 
 func init() {
@@ -75,9 +76,12 @@ var EppUnAvailableFailOpen = suite.ConformanceTest{
 				s.TimeoutConfig,
 				gwAddr,
 				trafficutils.Request{
-					Host:      hostname,
-					Path:      path,
-					Headers:   map[string]string{testfilter.HeaderTestEppEndPointSelectionKey: targetPodIP},
+					Host: hostname,
+					Path: path,
+					Headers: map[string]string{
+						test.HeaderTestEppEndPointSelectionKey: targetPodIP,
+						metadata.ObjectiveKey:                  resources.InferenceObjName,
+					},
 					Method:    http.MethodPost,
 					Body:      requestBody,
 					Backend:   pods[0].Name, // Make sure the request is from the targetPod when the EPP is alive.
@@ -100,9 +104,12 @@ var EppUnAvailableFailOpen = suite.ConformanceTest{
 				s.TimeoutConfig,
 				gwAddr,
 				trafficutils.Request{
-					Host:      hostname,
-					Path:      path,
-					Headers:   map[string]string{testfilter.HeaderTestEppEndPointSelectionKey: targetPodIP},
+					Host: hostname,
+					Path: path,
+					Headers: map[string]string{
+						test.HeaderTestEppEndPointSelectionKey: targetPodIP,
+						metadata.ObjectiveKey:                  resources.InferenceObjName,
+					},
 					Method:    http.MethodPost,
 					Body:      requestBody,
 					Backend:   appPodBackendPrefix, // Only checks the prefix since the EPP is not alive and the response can return from any Pod.
diff --git a/conformance/tests/epp_unavailable_fail_open.yaml b/conformance/tests/epp_unavailable_fail_open.yaml
index 225dbba..93f3e86 100644
--- a/conformance/tests/epp_unavailable_fail_open.yaml
+++ b/conformance/tests/epp_unavailable_fail_open.yaml
@@ -6,7 +6,7 @@ metadata:
   name: conformance-fake-model-server
   namespace: gateway-conformance-app-backend
 spec:
-  criticality: Critical # Mark it as critical to bypass the saturation check since the model server is fake and don't have such metrics. 
+  criticality: 2 # Mark criticality high enough to bypass the saturation check since the model server is fake and don't have such metrics. 
   poolRef:
     name: secondary-inference-pool
 ---
diff --git a/conformance/tests/gateway_following_epp_routing.go b/conformance/tests/gateway_following_epp_routing.go
index 95b894c..30a6dde 100644
--- a/conformance/tests/gateway_following_epp_routing.go
+++ b/conformance/tests/gateway_following_epp_routing.go
@@ -33,7 +33,8 @@ import (
 	"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"
-	testfilter "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test/filter"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test"
 )
 
 func init() {
@@ -92,9 +93,12 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{
 				s.TimeoutConfig,
 				gwAddr,
 				traffic.Request{
-					Host:      hostname,
-					Path:      path,
-					Headers:   map[string]string{testfilter.HeaderTestEppEndPointSelectionKey: podIPs[i]},
+					Host: hostname,
+					Path: path,
+					Headers: map[string]string{
+						test.HeaderTestEppEndPointSelectionKey: podIPs[i],
+						metadata.ObjectiveKey:                  resources.InferenceObjName,
+					},
 					Method:    http.MethodPost,
 					Body:      requestBody,
 					Backend:   podNames[i],
@@ -128,9 +132,12 @@ var GatewayFollowingEPPRouting = suite.ConformanceTest{
 		for _, tc := range testCases {
 			t.Run(tc.name, func(t *testing.T) {
 				eppHeaderValue := strings.Join(tc.podIPsToBeReturnedByEPP, ",")
-				headers := map[string]string{testfilter.HeaderTestEppEndPointSelectionKey: eppHeaderValue}
+				headers := map[string]string{
+					test.HeaderTestEppEndPointSelectionKey: eppHeaderValue,
+					metadata.ObjectiveKey:                  resources.InferenceObjName,
+				}
 
-				t.Logf("Sending request to %s with EPP header '%s: %s'", gwAddr, testfilter.HeaderTestEppEndPointSelectionKey, eppHeaderValue)
+				t.Logf("Sending request to %s with EPP header '%s: %s'", gwAddr, test.HeaderTestEppEndPointSelectionKey, eppHeaderValue)
 				t.Logf("Expecting traffic to be routed to pod: %v", tc.expectAllRequestsRoutedWithinPodNames)
 
 				assertTrafficOnlyReachesToExpectedPods(t, s, gwAddr, gwhttp.ExpectedResponse{
diff --git a/conformance/tests/gateway_following_epp_routing.yaml b/conformance/tests/gateway_following_epp_routing.yaml
index 9e3a43e..37b00e7 100644
--- a/conformance/tests/gateway_following_epp_routing.yaml
+++ b/conformance/tests/gateway_following_epp_routing.yaml
@@ -6,7 +6,7 @@ metadata:
   name: conformance-fake-model-server
   namespace: gateway-conformance-app-backend
 spec:
-  criticality: Critical # Mark it as critical to bypass the saturation check since the model server is fake and don't have such metrics. 
+  criticality: 2 # Mark criticality high enough to bypass the saturation check since the model server is fake and don't have such metrics.
   poolRef:
     name: primary-inference-pool
 ---
diff --git a/pkg/epp/backend/metrics/pod_metrics_test.go b/pkg/epp/backend/metrics/pod_metrics_test.go
index 1c4a339..9a0e1a6 100644
--- a/pkg/epp/backend/metrics/pod_metrics_test.go
+++ b/pkg/epp/backend/metrics/pod_metrics_test.go
@@ -62,10 +62,10 @@ var (
 func TestMetricsRefresh(t *testing.T) {
 	ctx := context.Background()
 	pmc := &FakePodMetricsClient{}
-	pmf := NewPodMetricsFactory(pmc, time.Millisecond, time.Second*2)
+	pmf := NewPodMetricsFactory(pmc, time.Millisecond)
 
 	// The refresher is initialized with empty metrics.
-	pm := pmf.NewPodMetrics(ctx, pod1, &fakeDataStore{})
+	pm := pmf.NewEndpoint(ctx, pod1, &fakeDataStore{})
 
 	namespacedName := types.NamespacedName{Name: pod1.Name, Namespace: pod1.Namespace}
 	// Use SetRes to simulate an update of metrics from the pod.
@@ -78,7 +78,7 @@ func TestMetricsRefresh(t *testing.T) {
 
 	// Stop the loop, and simulate metric update again, this time the PodMetrics won't get the
 	// new update.
-	pm.StopRefreshLoop()
+	pmf.ReleaseEndpoint(pm)
 	time.Sleep(pmf.refreshMetricsInterval * 2 /* small buffer for robustness */)
 	pmc.SetRes(map[types.NamespacedName]*MetricsState{namespacedName: updated})
 	// Still expect the same condition (no metrics update).
@@ -88,7 +88,7 @@ func TestMetricsRefresh(t *testing.T) {
 type fakeDataStore struct{}
 
 func (f *fakeDataStore) PoolGet() (*v1.InferencePool, error) {
-	return &v1.InferencePool{Spec: v1.InferencePoolSpec{TargetPortNumber: 8000}}, nil
+	return &v1.InferencePool{Spec: v1.InferencePoolSpec{TargetPorts: []v1.Port{{Number: 8000}}}}, nil
 }
 
 func (f *fakeDataStore) PodList(func(PodMetrics) bool) []PodMetrics {
diff --git a/pkg/epp/controller/inferenceobjective_reconciler_test.go b/pkg/epp/controller/inferenceobjective_reconciler_test.go
index ad068af..de43d6e 100644
--- a/pkg/epp/controller/inferenceobjective_reconciler_test.go
+++ b/pkg/epp/controller/inferenceobjective_reconciler_test.go
@@ -43,29 +43,39 @@ var (
 	pool          = utiltest.MakeInferencePool("test-pool1").Namespace("ns1").ObjRef()
 	infObjective1 = utiltest.MakeInferenceObjective("model1").
 			Namespace(pool.Namespace).
-			Criticality(v1alpha2.Standard).
+			Priority(1).
 			CreationTimestamp(metav1.Unix(1000, 0)).
-			PoolName(pool.Name).ObjRef()
+			PoolName(pool.Name).
+			PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1Pool2 = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(infObjective1.Namespace).
-				Criticality(*infObjective1.Spec.Criticality).
+				Priority(*infObjective1.Spec.Priority).
 				CreationTimestamp(metav1.Unix(1001, 0)).
-				PoolName("test-pool2").ObjRef()
+				PoolName("test-pool2").
+				PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1Critical = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(infObjective1.Namespace).
-				Criticality(v1alpha2.Critical).
+				Priority(2).
 				CreationTimestamp(metav1.Unix(1003, 0)).
-				PoolName(pool.Name).ObjRef()
+				PoolName(pool.Name).
+				PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1Deleted = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(infObjective1.Namespace).
 				CreationTimestamp(metav1.Unix(1004, 0)).
 				DeletionTimestamp().
-				PoolName(pool.Name).ObjRef()
-
+				PoolName(pool.Name).
+				PoolGroup("inference.networking.k8s.io").ObjRef()
+	infObjective1DiffGroup = utiltest.MakeInferenceObjective(infObjective1.Name).
+				Namespace(pool.Namespace).
+				Priority(1).
+				CreationTimestamp(metav1.Unix(1005, 0)).
+				PoolName(pool.Name).
+				PoolGroup("inference.networking.x-k8s.io").ObjRef()
 	infObjective2 = utiltest.MakeInferenceObjective("model2").
 			Namespace(pool.Namespace).
 			CreationTimestamp(metav1.Unix(1000, 0)).
-			PoolName(pool.Name).ObjRef()
+			PoolName(pool.Name).
+			PoolGroup("inference.networking.k8s.io").ObjRef()
 )
 
 func TestInferenceObjectiveReconciler(t *testing.T) {
@@ -102,7 +112,7 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			wantObjectives:     []*v1alpha2.InferenceObjective{},
 		},
 		{
-			name:               "Objective changed criticality",
+			name:               "Objective changed priority",
 			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
 			objective:          infObjective1Critical,
 			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1Critical},
@@ -119,6 +129,17 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			objective:          infObjective2,
 			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1, infObjective2},
 		},
+		{
+			name:               "Objective deleted due to group mismatch for the inference pool",
+			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
+			objective:          infObjective1DiffGroup,
+			wantObjectives:     []*v1alpha2.InferenceObjective{},
+		},
+		{
+			name:           "Objective ignored due to group mismatch for the inference pool",
+			objective:      infObjective1DiffGroup,
+			wantObjectives: []*v1alpha2.InferenceObjective{},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
@@ -134,12 +155,11 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			for _, m := range test.objectivesInAPIServer {
 				initObjs = append(initObjs, m)
 			}
-
 			fakeClient := fake.NewClientBuilder().
 				WithScheme(scheme).
 				WithObjects(initObjs...).
 				Build()
-			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			ds := datastore.NewDatastore(t.Context(), pmf)
 			for _, m := range test.objectivessInStore {
 				ds.ObjectiveSet(m)
diff --git a/pkg/epp/controller/inferencepool_reconciler_test.go b/pkg/epp/controller/inferencepool_reconciler_test.go
index a29d531..48d5083 100644
--- a/pkg/epp/controller/inferencepool_reconciler_test.go
+++ b/pkg/epp/controller/inferencepool_reconciler_test.go
@@ -80,9 +80,10 @@ func TestInferencePoolReconciler(t *testing.T) {
 	pool1 := utiltest.MakeInferencePool("pool1").
 		Namespace("pool1-ns").
 		Selector(selector_v1).
-		TargetPortNumber(8080).ObjRef()
+		TargetPorts(8080).
+		ExtensionRef("epp-service").ObjRef()
 	pool1.SetGroupVersionKind(gvk)
-	pool2 := utiltest.MakeInferencePool("pool2").Namespace("pool2-ns").ObjRef()
+	pool2 := utiltest.MakeInferencePool("pool2").Namespace("pool2-ns").ExtensionRef("epp-service").ObjRef()
 	pool2.SetGroupVersionKind(gvk)
 
 	// Set up the scheme.
@@ -111,7 +112,7 @@ func TestInferencePoolReconciler(t *testing.T) {
 	req := ctrl.Request{NamespacedName: namespacedName}
 	ctx := context.Background()
 
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 	datastore := datastore.NewDatastore(ctx, pmf)
 	inferencePoolReconciler := &InferencePoolReconciler{Reader: fakeClient, Datastore: datastore, PoolGKNN: gknn}
 
@@ -127,7 +128,9 @@ func TestInferencePoolReconciler(t *testing.T) {
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
 		t.Errorf("Unexpected pool get error: %v", err)
 	}
-	newPool1.Spec.Selector = map[v1.LabelKey]v1.LabelValue{"app": "vllm_v2"}
+	newPool1.Spec.Selector = v1.LabelSelector{
+		MatchLabels: map[v1.LabelKey]v1.LabelValue{"app": "vllm_v2"},
+	}
 	if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil {
 		t.Errorf("Unexpected pool update error: %v", err)
 	}
@@ -143,7 +146,7 @@ func TestInferencePoolReconciler(t *testing.T) {
 	if err := fakeClient.Get(ctx, req.NamespacedName, newPool1); err != nil {
 		t.Errorf("Unexpected pool get error: %v", err)
 	}
-	newPool1.Spec.TargetPortNumber = 9090
+	newPool1.Spec.TargetPorts = []v1.Port{{Number: 9090}}
 	if err := fakeClient.Update(ctx, newPool1, &client.UpdateOptions{}); err != nil {
 		t.Errorf("Unexpected pool update error: %v", err)
 	}
@@ -186,7 +189,7 @@ func diffStore(datastore datastore.Datastore, params diffStoreParams) string {
 		params.wantPods = []string{}
 	}
 	gotPods := []string{}
-	for _, pm := range datastore.PodList(backendmetrics.AllPodPredicate) {
+	for _, pm := range datastore.PodList(backendmetrics.AllPodsPredicate) {
 		gotPods = append(gotPods, pm.GetPod().NamespacedName.Name)
 	}
 	if diff := cmp.Diff(params.wantPods, gotPods, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
@@ -216,11 +219,15 @@ func TestXInferencePoolReconciler(t *testing.T) {
 		Version: v1alpha2.GroupVersion.Version,
 		Kind:    "InferencePool",
 	}
-	pool1 := utiltest.MakeXInferencePool("pool1").
+	pool1 := utiltest.MakeAlphaInferencePool("pool1").
 		Namespace("pool1-ns").
 		Selector(selector_v1).
+		ExtensionRef("epp-service").
+		TargetPortNumber(8080).ObjRef()
+	pool2 := utiltest.MakeAlphaInferencePool("pool2").
+		Namespace("pool2-ns").
+		ExtensionRef("epp-service").
 		TargetPortNumber(8080).ObjRef()
-	pool2 := utiltest.MakeXInferencePool("pool2").Namespace("pool2-ns").ObjRef()
 	pool1.SetGroupVersionKind(gvk)
 	pool2.SetGroupVersionKind(gvk)
 
@@ -250,7 +257,7 @@ func TestXInferencePoolReconciler(t *testing.T) {
 	req := ctrl.Request{NamespacedName: namespacedName}
 	ctx := context.Background()
 
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 	datastore := datastore.NewDatastore(ctx, pmf)
 	inferencePoolReconciler := &InferencePoolReconciler{Reader: fakeClient, Datastore: datastore, PoolGKNN: gknn}
 
@@ -319,13 +326,12 @@ func xDiffStore(t *testing.T, datastore datastore.Datastore, params xDiffStorePa
 	if gotPool == nil && params.wantPool == nil {
 		return ""
 	}
-	uns, err := common.ToUnstructured(gotPool)
-	if err != nil {
-		t.Fatalf("failed to convert XInferencePool to Unstructured: %v", err)
-	}
-	gotXPool, err := common.ToXInferencePool(uns)
+
+	gotXPool := &v1alpha2.InferencePool{}
+
+	err := gotXPool.ConvertFrom(gotPool)
 	if err != nil {
-		t.Fatalf("failed to convert unstructured to InferencePool: %v", err)
+		t.Fatalf("failed to convert InferencePool to XInferencePool: %v", err)
 	}
 	if diff := cmp.Diff(params.wantPool, gotXPool); diff != "" {
 		return "pool:" + diff
@@ -336,7 +342,7 @@ func xDiffStore(t *testing.T, datastore datastore.Datastore, params xDiffStorePa
 		params.wantPods = []string{}
 	}
 	gotPods := []string{}
-	for _, pm := range datastore.PodList(backendmetrics.AllPodPredicate) {
+	for _, pm := range datastore.PodList(backendmetrics.AllPodsPredicate) {
 		gotPods = append(gotPods, pm.GetPod().NamespacedName.Name)
 	}
 	if diff := cmp.Diff(params.wantPods, gotPods, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
diff --git a/pkg/epp/controller/pod_reconciler_test.go b/pkg/epp/controller/pod_reconciler_test.go
index 0f62021..5ceb3ef 100644
--- a/pkg/epp/controller/pod_reconciler_test.go
+++ b/pkg/epp/controller/pod_reconciler_test.go
@@ -44,7 +44,7 @@ var (
 	basePod3  = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod3"}, Status: corev1.PodStatus{PodIP: "address-3"}}
 	basePod11 = &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, Status: corev1.PodStatus{PodIP: "address-11"}}
 	pmc       = &backendmetrics.FakePodMetricsClient{}
-	pmf       = backendmetrics.NewPodMetricsFactory(pmc, time.Second, time.Second*2)
+	pmf       = backendmetrics.NewPodMetricsFactory(pmc, time.Second)
 )
 
 func TestPodReconciler(t *testing.T) {
@@ -61,9 +61,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -77,9 +79,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -93,9 +97,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -110,9 +116,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -124,9 +132,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -139,9 +149,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -155,9 +167,11 @@ func TestPodReconciler(t *testing.T) {
 			existingPods: []*corev1.Pod{basePod1, basePod2},
 			pool: &v1.InferencePool{
 				Spec: v1.InferencePoolSpec{
-					TargetPortNumber: int32(8000),
-					Selector: map[v1.LabelKey]v1.LabelValue{
-						"some-key": "some-val",
+					TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+					Selector: v1.LabelSelector{
+						MatchLabels: map[v1.LabelKey]v1.LabelValue{
+							"some-key": "some-val",
+						},
 					},
 				},
 			},
@@ -198,7 +212,7 @@ func TestPodReconciler(t *testing.T) {
 			}
 
 			var gotPods []*corev1.Pod
-			for _, pm := range store.PodList(backendmetrics.AllPodPredicate) {
+			for _, pm := range store.PodList(backendmetrics.AllPodsPredicate) {
 				pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: pm.GetPod().NamespacedName.Name, Namespace: pm.GetPod().NamespacedName.Namespace}, Status: corev1.PodStatus{PodIP: pm.GetPod().Address}}
 				gotPods = append(gotPods, pod)
 			}
diff --git a/pkg/epp/datalayer/collector_test.go b/pkg/epp/datalayer/collector_test.go
index b5f348d..2d47de3 100644
--- a/pkg/epp/datalayer/collector_test.go
+++ b/pkg/epp/datalayer/collector_test.go
@@ -98,15 +98,16 @@ func TestCollectorCollectsOnTicks(t *testing.T) {
 	c := NewCollector()
 	ticker := mocks.NewTicker()
 	ctx := context.Background()
-	require.NoError(t, c.Start(ctx, ticker, endpoint, []DataSource{source}))
 
+	require.NoError(t, c.Start(ctx, ticker, endpoint, []DataSource{source}))
 	ticker.Tick()
 	ticker.Tick()
-	time.Sleep(20 * time.Millisecond) // let collector process the ticks
 
-	got := atomic.LoadInt64(&source.callCount)
-	want := int64(2)
-	assert.Equal(t, want, got, "call count mismatch")
+	// use Eventually for async processing
+	require.Eventually(t, func() bool {
+		return atomic.LoadInt64(&source.callCount) == 2
+	}, 1*time.Second, 2*time.Millisecond, "expected 2 collections")
+
 	require.NoError(t, c.Stop())
 }
 
diff --git a/pkg/epp/datastore/datastore_test.go b/pkg/epp/datastore/datastore_test.go
index e402fc4..271c31e 100644
--- a/pkg/epp/datastore/datastore_test.go
+++ b/pkg/epp/datastore/datastore_test.go
@@ -82,7 +82,7 @@ func TestPool(t *testing.T) {
 			fakeClient := fake.NewClientBuilder().
 				WithScheme(scheme).
 				Build()
-			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			datastore := NewDatastore(context.Background(), pmf)
 			_ = datastore.PoolSet(context.Background(), fakeClient, tt.inferencePool)
 			gotPool, gotErr := datastore.PoolGet()
@@ -114,7 +114,7 @@ func TestObjective(t *testing.T) {
 	model2ts := testutil.MakeInferenceObjective("model2").ObjRef()
 	// Same model name as model1ts, newer timestamp
 	model1tsCritical := testutil.MakeInferenceObjective("model1").
-		Criticality(v1alpha2.Critical).ObjRef()
+		Priority(2).ObjRef()
 	// Same object name as model2ts, different model name.
 	model2chat := testutil.MakeInferenceObjective(model2ts.Name).ObjRef()
 
@@ -135,7 +135,7 @@ func TestObjective(t *testing.T) {
 			wantOpResult: true,
 		},
 		{
-			name:           "Set model1 with the same modelName, but with diff criticality, should update.",
+			name:           "Set model1 with the same modelName, but with diff priority, should update.",
 			existingModels: []*v1alpha2.InferenceObjective{model1ts},
 			op: func(ds Datastore) bool {
 				ds.ObjectiveSet(model1tsCritical)
@@ -189,7 +189,7 @@ func TestObjective(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			ds := NewDatastore(t.Context(), pmf)
 			for _, m := range test.existingModels {
 				ds.ObjectiveSet(m)
@@ -245,7 +245,7 @@ var (
 	pod2NamespacedName = types.NamespacedName{Name: pod2.Name, Namespace: pod2.Namespace}
 	inferencePool      = &v1.InferencePool{
 		Spec: v1.InferencePoolSpec{
-			TargetPortNumber: 8000,
+			TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
 		},
 	}
 )
@@ -256,6 +256,7 @@ func TestMetrics(t *testing.T) {
 		pmc       backendmetrics.PodMetricsClient
 		storePods []*corev1.Pod
 		want      []*backendmetrics.MetricsState
+		predict   func(backendmetrics.PodMetrics) bool
 	}{
 		{
 			name: "Probing metrics success",
@@ -313,15 +314,18 @@ func TestMetrics(t *testing.T) {
 			fakeClient := fake.NewClientBuilder().
 				WithScheme(scheme).
 				Build()
-			pmf := backendmetrics.NewPodMetricsFactory(test.pmc, time.Millisecond, time.Second*2)
+			pmf := backendmetrics.NewPodMetricsFactory(test.pmc, time.Millisecond)
 			ds := NewDatastore(ctx, pmf)
 			_ = ds.PoolSet(ctx, fakeClient, inferencePool)
 			for _, pod := range test.storePods {
 				ds.PodUpdateOrAddIfNotExist(pod)
 			}
 			time.Sleep(1 * time.Second) // Give some time for the metrics to be fetched.
+			if test.predict == nil {
+				test.predict = backendmetrics.AllPodsPredicate
+			}
 			assert.EventuallyWithT(t, func(t *assert.CollectT) {
-				got := ds.PodList(backendmetrics.AllPodPredicate)
+				got := ds.PodList(test.predict)
 				metrics := []*backendmetrics.MetricsState{}
 				for _, one := range got {
 					metrics = append(metrics, one.GetMetrics())
@@ -407,7 +411,7 @@ func TestPods(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			ctx := context.Background()
-			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 			ds := NewDatastore(t.Context(), pmf)
 			for _, pod := range test.existingPods {
 				ds.PodUpdateOrAddIfNotExist(pod)
@@ -415,7 +419,7 @@ func TestPods(t *testing.T) {
 
 			test.op(ctx, ds)
 			var gotPods []*corev1.Pod
-			for _, pm := range ds.PodList(backendmetrics.AllPodPredicate) {
+			for _, pm := range ds.PodList(backendmetrics.AllPodsPredicate) {
 				pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: pm.GetPod().NamespacedName.Name, Namespace: pm.GetPod().NamespacedName.Namespace}, Status: corev1.PodStatus{PodIP: pm.GetPod().Address}}
 				gotPods = append(gotPods, pod)
 			}
diff --git a/pkg/epp/flowcontrol/controller/internal/filter_test.go b/pkg/epp/flowcontrol/controller/internal/filter_test.go
index f51581e..ceff9e8 100644
--- a/pkg/epp/flowcontrol/controller/internal/filter_test.go
+++ b/pkg/epp/flowcontrol/controller/internal/filter_test.go
@@ -35,22 +35,22 @@ func TestNewSaturationFilter(t *testing.T) {
 	t.Parallel()
 
 	testCases := []struct {
-		name              string
-		isSaturated       bool
-		expectShouldPause bool
-		expectAllowed     map[string]struct{}
+		name                  string
+		isSaturated           bool
+		expectShouldPause     bool
+		expectFilteredBandNil bool
 	}{
 		{
-			name:              "should not pause or filter when system is not saturated",
-			isSaturated:       false,
-			expectShouldPause: false,
-			expectAllowed:     nil, // nil map signals the fast path
+			name:                  "should not pause or filter when system is not saturated",
+			isSaturated:           false,
+			expectShouldPause:     false,
+			expectFilteredBandNil: true, // nil band signals the fast path
 		},
 		{
-			name:              "should pause when system is saturated",
-			isSaturated:       true,
-			expectShouldPause: true,
-			expectAllowed:     nil,
+			name:                  "should pause when system is saturated",
+			isSaturated:           true,
+			expectShouldPause:     true,
+			expectFilteredBandNil: true,
 		},
 	}
 
@@ -66,15 +66,15 @@ func TestNewSaturationFilter(t *testing.T) {
 			mockBand := &frameworkmocks.MockPriorityBandAccessor{}
 
 			// --- ACT ---
-			allowed, shouldPause := filter(context.Background(), mockBand, logr.Discard())
+			filteredBand, shouldPause := filter(context.Background(), mockBand, logr.Discard())
 
 			// --- ASSERT ---
 			assert.Equal(t, tc.expectShouldPause, shouldPause, "The filter's pause signal should match the expected value")
 
-			if tc.expectAllowed == nil {
-				assert.Nil(t, allowed, "Expected allowed map to be nil for the fast path")
+			if tc.expectFilteredBandNil {
+				assert.Nil(t, filteredBand, "Expected filtered band to be nil")
 			} else {
-				assert.Equal(t, tc.expectAllowed, allowed, "The set of allowed flows should match the expected value")
+				assert.NotNil(t, filteredBand, "Expected a non-nil filtered band")
 			}
 		})
 	}
@@ -85,15 +85,19 @@ func TestSubsetPriorityBandAccessor(t *testing.T) {
 
 	// --- ARRANGE ---
 	// Setup a mock original accessor that knows about three flows.
-	mockQueueA := &frameworkmocks.MockFlowQueueAccessor{FlowSpecV: types.FlowSpecification{ID: "flow-a"}}
-	mockQueueB := &frameworkmocks.MockFlowQueueAccessor{FlowSpecV: types.FlowSpecification{ID: "flow-b"}}
-	mockQueueC := &frameworkmocks.MockFlowQueueAccessor{FlowSpecV: types.FlowSpecification{ID: "flow-c"}}
+	flowAKey := types.FlowKey{ID: "flow-a", Priority: 10}
+	flowBKey := types.FlowKey{ID: "flow-b", Priority: 10}
+	flowCKey := types.FlowKey{ID: "flow-c", Priority: 10}
+
+	mockQueueA := &frameworkmocks.MockFlowQueueAccessor{FlowKeyV: flowAKey}
+	mockQueueB := &frameworkmocks.MockFlowQueueAccessor{FlowKeyV: flowBKey}
+	mockQueueC := &frameworkmocks.MockFlowQueueAccessor{FlowKeyV: flowCKey}
 
 	originalAccessor := &frameworkmocks.MockPriorityBandAccessor{
 		PriorityV:     10,
 		PriorityNameV: "High",
-		FlowIDsFunc: func() []string {
-			return []string{"flow-a", "flow-b", "flow-c"}
+		FlowKeysFunc: func() []types.FlowKey {
+			return []types.FlowKey{flowAKey, flowBKey, flowCKey}
 		},
 		QueueFunc: func(id string) framework.FlowQueueAccessor {
 			switch id {
@@ -118,10 +122,7 @@ func TestSubsetPriorityBandAccessor(t *testing.T) {
 	}
 
 	// Create a subset view that only allows two of the flows.
-	allowedFlows := map[string]struct{}{
-		"flow-a": {},
-		"flow-c": {},
-	}
+	allowedFlows := []types.FlowKey{flowAKey, flowCKey}
 	subsetAccessor := newSubsetPriorityBandAccessor(originalAccessor, allowedFlows)
 	require.NotNil(t, subsetAccessor, "newSubsetPriorityBandAccessor should not return nil")
 
@@ -132,12 +133,14 @@ func TestSubsetPriorityBandAccessor(t *testing.T) {
 			"PriorityName() should pass through from the original accessor")
 	})
 
-	t.Run("should only return allowed flow IDs", func(t *testing.T) {
+	t.Run("should only return allowed flow keys", func(t *testing.T) {
 		t.Parallel()
-		flowIDs := subsetAccessor.FlowIDs()
+		flowKeys := subsetAccessor.FlowKeys()
 		// Sort for consistent comparison, as the pre-computed slice order is not guaranteed.
-		sort.Strings(flowIDs)
-		assert.Equal(t, []string{"flow-a", "flow-c"}, flowIDs, "FlowIDs() should only return the allowed subset")
+		sort.Slice(flowKeys, func(i, j int) bool {
+			return flowKeys[i].ID < flowKeys[j].ID
+		})
+		assert.Equal(t, []types.FlowKey{flowAKey, flowCKey}, flowKeys, "FlowKeys() should only return the allowed subset")
 	})
 
 	t.Run("should only return queues for allowed flows", func(t *testing.T) {
@@ -151,7 +154,7 @@ func TestSubsetPriorityBandAccessor(t *testing.T) {
 		t.Parallel()
 		var iterated []string
 		subsetAccessor.IterateQueues(func(queue framework.FlowQueueAccessor) bool {
-			iterated = append(iterated, queue.FlowSpec().ID)
+			iterated = append(iterated, queue.FlowKey().ID)
 			return true
 		})
 		// Sort for consistent comparison, as iteration order is not guaranteed.
@@ -163,7 +166,7 @@ func TestSubsetPriorityBandAccessor(t *testing.T) {
 		t.Parallel()
 		var iterated []string
 		subsetAccessor.IterateQueues(func(queue framework.FlowQueueAccessor) bool {
-			iterated = append(iterated, queue.FlowSpec().ID)
+			iterated = append(iterated, queue.FlowKey().ID)
 			return false // Exit after the first item.
 		})
 		assert.Len(t, iterated, 1, "Iteration should have stopped after one item")
diff --git a/pkg/epp/flowcontrol/controller/internal/item_test.go b/pkg/epp/flowcontrol/controller/internal/item_test.go
index 11e08ae..d50aaed 100644
--- a/pkg/epp/flowcontrol/controller/internal/item_test.go
+++ b/pkg/epp/flowcontrol/controller/internal/item_test.go
@@ -41,7 +41,8 @@ func TestItem(t *testing.T) {
 
 	t.Run("should have a non-finalized state upon creation", func(t *testing.T) {
 		t.Parallel()
-		req := typesmocks.NewMockFlowControlRequest(100, "req-1", "flow-a", context.Background())
+		key := types.FlowKey{ID: "flow-a", Priority: 10}
+		req := typesmocks.NewMockFlowControlRequest(100, "req-1", key, context.Background())
 		item := NewItem(req, time.Minute, time.Now())
 		require.NotNil(t, item, "NewItem should not return nil")
 		outcome, err := item.FinalState()
diff --git a/pkg/epp/flowcontrol/controller/internal/processor_test.go b/pkg/epp/flowcontrol/controller/internal/processor_test.go
index 09f1206..67657a9 100644
--- a/pkg/epp/flowcontrol/controller/internal/processor_test.go
+++ b/pkg/epp/flowcontrol/controller/internal/processor_test.go
@@ -33,7 +33,7 @@ limitations under the License.
 //     ensures that tests are verifying the processor's orchestration logic (i.e., that it calls its dependencies
 //     correctly) and are not affected by confounding bugs in those dependencies.
 //
-// In summary, this strategy is a prerequisite for reliably testing a concurrent engine, not just a simple data
+// In summary, this is a prerequisite for reliably testing a concurrent engine, not just a simple data
 // structure.
 //
 
@@ -71,7 +71,7 @@ const (
 	testWaitTimeout = 1 * time.Second
 )
 
-var testFlow = types.FlowSpecification{ID: "flow-a", Priority: 10}
+var testFlow = types.FlowKey{ID: "flow-a", Priority: 10}
 
 // TestMain sets up the logger for all tests in the package.
 func TestMain(m *testing.M) {
@@ -121,8 +121,8 @@ type testHarness struct {
 	// --- Centralized Mock State ---
 	// The harness's mutex protects the single source of truth for all mock state.
 	mu            sync.Mutex
-	queues        map[string]*mocks.MockManagedQueue // Key: `flowID`
-	priorityFlows map[uint][]string                  // Key: `priority`, Val: slice of `flowIDs`
+	queues        map[types.FlowKey]*mocks.MockManagedQueue
+	priorityFlows map[uint][]types.FlowKey // Key: `priority`
 
 	// Customizable policy logic for tests to override.
 	interFlowPolicySelectQueue func(band framework.PriorityBandAccessor) (framework.FlowQueueAccessor, error)
@@ -138,12 +138,11 @@ func newTestHarness(t *testing.T, expiryCleanupInterval time.Duration) *testHarn
 		mockClock:         newMockClock(),
 		logger:            logr.Discard(),
 		startSignal:       make(chan struct{}),
-		queues:            make(map[string]*mocks.MockManagedQueue),
-		priorityFlows:     make(map[uint][]string),
+		queues:            make(map[types.FlowKey]*mocks.MockManagedQueue),
+		priorityFlows:     make(map[uint][]types.FlowKey),
 	}
 
 	// Wire up the harness to provide the mock implementations for the shard's dependencies.
-	h.ActiveManagedQueueFunc = h.activeManagedQueue
 	h.ManagedQueueFunc = h.managedQueue
 	h.AllOrderedPriorityLevelsFunc = h.allOrderedPriorityLevels
 	h.PriorityBandAccessorFunc = h.priorityBandAccessor
@@ -165,7 +164,7 @@ func newTestHarness(t *testing.T, expiryCleanupInterval time.Duration) *testHarn
 		ctx context.Context,
 		band framework.PriorityBandAccessor,
 		logger logr.Logger,
-	) (map[string]struct{}, bool) {
+	) (framework.PriorityBandAccessor, bool) {
 		return nil, false
 	}
 	h.processor = NewShardProcessor(h, filter, h.mockClock, expiryCleanupInterval, h.logger)
@@ -215,48 +214,38 @@ func (h *testHarness) waitForFinalization(item *flowItem) (types.QueueOutcome, e
 }
 
 // newTestItem creates a new flowItem for testing purposes.
-func (h *testHarness) newTestItem(id, flowID string, ttl time.Duration) *flowItem {
+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, flowID, ctx)
+	req := typesmocks.NewMockFlowControlRequest(100, id, key, ctx)
 	return NewItem(req, ttl, h.mockClock.Now())
 }
 
 // addQueue centrally registers a new mock queue for a given flow, ensuring all harness components are aware of it.
-func (h *testHarness) addQueue(spec types.FlowSpecification) *mocks.MockManagedQueue {
+func (h *testHarness) addQueue(key types.FlowKey) *mocks.MockManagedQueue {
 	h.t.Helper()
 	h.mu.Lock()
 	defer h.mu.Unlock()
 
-	mockQueue := &mocks.MockManagedQueue{FlowSpecV: spec}
-	h.queues[spec.ID] = mockQueue
+	mockQueue := &mocks.MockManagedQueue{FlowKeyV: key}
+	h.queues[key] = mockQueue
 
-	// Add the `flowID` to the correct priority band, creating the band if needed.
-	h.priorityFlows[spec.Priority] = append(h.priorityFlows[spec.Priority], spec.ID)
+	// Add the key to the correct priority band, creating the band if needed.
+	h.priorityFlows[key.Priority] = append(h.priorityFlows[key.Priority], key)
 
 	return mockQueue
 }
 
 // --- Mock Interface Implementations ---
 
-// activeManagedQueue provides the mock implementation for the `RegistryShard` interface.
-func (h *testHarness) activeManagedQueue(flowID string) (contracts.ManagedQueue, error) {
-	h.mu.Lock()
-	defer h.mu.Unlock()
-	if q, ok := h.queues[flowID]; ok {
-		return q, nil
-	}
-	return nil, fmt.Errorf("test setup error: no active queue for flow %q", flowID)
-}
-
 // managedQueue provides the mock implementation for the `RegistryShard` interface.
-func (h *testHarness) managedQueue(flowID string, priority uint) (contracts.ManagedQueue, error) {
+func (h *testHarness) managedQueue(key types.FlowKey) (contracts.ManagedQueue, error) {
 	h.mu.Lock()
 	defer h.mu.Unlock()
-	if q, ok := h.queues[flowID]; ok && q.FlowSpec().Priority == priority {
+	if q, ok := h.queues[key]; ok {
 		return q, nil
 	}
-	return nil, fmt.Errorf("test setup error: no queue for %q at priority %d", flowID, priority)
+	return nil, fmt.Errorf("test setup error: no queue for %q", key)
 }
 
 // allOrderedPriorityLevels provides the mock implementation for the `RegistryShard` interface.
@@ -278,15 +267,15 @@ func (h *testHarness) priorityBandAccessor(p uint) (framework.PriorityBandAccess
 
 	// Safely get a snapshot of the flow IDs under a lock.
 	h.mu.Lock()
-	flowIDsForPriority := h.priorityFlows[p]
+	flowKeysForPriority := h.priorityFlows[p]
 	h.mu.Unlock()
 
 	// Configure the mock's behavior with a closure that reads from the harness's centralized, thread-safe state.
 	band.IterateQueuesFunc = func(cb func(fqa framework.FlowQueueAccessor) bool) {
 		// This closure safely iterates over the snapshot of flow IDs.
-		for _, id := range flowIDsForPriority {
+		for _, key := range flowKeysForPriority {
 			// Get the queue using the thread-safe `managedQueue` method.
-			q, err := h.managedQueue(id, p)
+			q, err := h.managedQueue(key)
 			if err == nil && q != nil {
 				mq := q.(*mocks.MockManagedQueue)
 				if !cb(mq.FlowQueueAccessor()) {
@@ -323,7 +312,7 @@ func (h *testHarness) interFlowDispatchPolicy(p uint) (framework.InterFlowDispat
 }
 
 // intraFlowDispatchPolicy provides the mock implementation for the `contracts.RegistryShard` interface.
-func (h *testHarness) intraFlowDispatchPolicy(flowID string, priority uint) (framework.IntraFlowDispatchPolicy, error) {
+func (h *testHarness) intraFlowDispatchPolicy(types.FlowKey) (framework.IntraFlowDispatchPolicy, error) {
 	policy := &frameworkmocks.MockIntraFlowDispatchPolicy{}
 	// If the test provided a custom implementation, use it.
 	if h.intraFlowPolicySelectItem != nil {
@@ -351,8 +340,8 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-dispatch-success", testFlow.ID, testTTL)
-			h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-dispatch-success", testFlow, testTTL)
+			h.addQueue(types.FlowKey{ID: testFlow.ID, Priority: testFlow.Priority})
 
 			// --- ACT ---
 			h.Start()
@@ -370,8 +359,8 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-capacity-reject", testFlow.ID, testTTL)
-			h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-capacity-reject", testFlow, testTTL)
+			h.addQueue(testFlow)
 			h.StatsFunc = func() contracts.ShardStats {
 				return contracts.ShardStats{PerPriorityBandStats: map[uint]contracts.PriorityBandStats{
 					testFlow.Priority: {CapacityBytes: 50}, // 50 is less than item size of 100
@@ -395,9 +384,9 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-lookup-fail-reject", testFlow.ID, testTTL)
+			item := h.newTestItem("req-lookup-fail-reject", testFlow, testTTL)
 			registryErr := errors.New("test registry lookup error")
-			h.ActiveManagedQueueFunc = func(flowID string) (contracts.ManagedQueue, error) {
+			h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) {
 				return nil, registryErr
 			}
 
@@ -418,8 +407,8 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-shutdown-reject", testFlow.ID, testTTL)
-			h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-shutdown-reject", testFlow, testTTL)
+			h.addQueue(testFlow)
 
 			// --- ACT ---
 			h.Start()
@@ -439,8 +428,8 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-expired-evict", testFlow.ID, testShortTTL)
-			h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-expired-evict", testFlow, testShortTTL)
+			h.addQueue(testFlow)
 
 			// --- ACT ---
 			h.Start()
@@ -463,8 +452,8 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, 1*time.Hour) // Disable background cleanup to isolate dispatch logic.
-			item := h.newTestItem("req-expired-dispatch-evict", testFlow.ID, testShortTTL)
-			mockQueue := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-expired-dispatch-evict", testFlow, testShortTTL)
+			mockQueue := h.addQueue(testFlow)
 			require.NoError(t, mockQueue.Add(item), "Adding item to mock queue should not fail")
 
 			// Have the policy select the item, but then advance time so it's expired by the time dispatchItem actually runs.
@@ -494,9 +483,9 @@ func TestShardProcessor(t *testing.T) {
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
 			ctx, cancel := context.WithCancel(context.Background())
-			req := typesmocks.NewMockFlowControlRequest(100, "req-ctx-cancel", testFlow.ID, ctx)
+			req := typesmocks.NewMockFlowControlRequest(100, "req-ctx-cancel", testFlow, ctx)
 			item := NewItem(req, testTTL, h.mockClock.Now())
-			h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			h.addQueue(testFlow)
 
 			// --- ACT ---
 			h.Start()
@@ -518,8 +507,8 @@ func TestShardProcessor(t *testing.T) {
 			t.Parallel()
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
-			item := h.newTestItem("req-shutdown-evict", testFlow.ID, testTTL)
-			mockQueue := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-shutdown-evict", testFlow, testTTL)
+			mockQueue := h.addQueue(testFlow)
 			require.NoError(t, mockQueue.Add(item), "Adding item to mock queue should not fail")
 
 			// Prevent dispatch to ensure we test shutdown eviction, not a successful dispatch.
@@ -544,10 +533,10 @@ func TestShardProcessor(t *testing.T) {
 			// --- ARRANGE ---
 			h := newTestHarness(t, testCleanupTick)
 			const numConcurrentItems = 20
-			q := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			q := h.addQueue(testFlow)
 			itemsToTest := make([]*flowItem, 0, numConcurrentItems)
 			for i := 0; i < numConcurrentItems; i++ {
-				item := h.newTestItem(fmt.Sprintf("req-concurrent-%d", i), testFlow.ID, testTTL)
+				item := h.newTestItem(fmt.Sprintf("req-concurrent-%d", i), testFlow, testTTL)
 				itemsToTest = append(itemsToTest, item)
 			}
 
@@ -581,8 +570,8 @@ func TestShardProcessor(t *testing.T) {
 
 			// --- ARRANGE ---
 			h := newTestHarness(t, 1*time.Hour) // Disable background cleanup to isolate the race.
-			item := h.newTestItem("req-race", testFlow.ID, testShortTTL)
-			q := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+			item := h.newTestItem("req-race", testFlow, testShortTTL)
+			q := h.addQueue(testFlow)
 
 			// Use channels to pause the dispatch cycle right before it would remove the item.
 			policyCanProceed := make(chan struct{})
@@ -683,7 +672,7 @@ func TestShardProcessor(t *testing.T) {
 				{
 					name: "should reject item on registry queue lookup failure",
 					setupHarness: func(h *testHarness) {
-						h.ActiveManagedQueueFunc = func(string) (contracts.ManagedQueue, error) { return nil, testErr }
+						h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) { return nil, testErr }
 					},
 					assert: func(t *testing.T, h *testHarness, item *flowItem) {
 						outcome, err := item.FinalState()
@@ -695,7 +684,7 @@ func TestShardProcessor(t *testing.T) {
 				{
 					name: "should reject item on registry priority band lookup failure",
 					setupHarness: func(h *testHarness) {
-						h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+						h.addQueue(testFlow)
 						h.PriorityBandAccessorFunc = func(uint) (framework.PriorityBandAccessor, error) { return nil, testErr }
 					},
 					assert: func(t *testing.T, h *testHarness, item *flowItem) {
@@ -708,7 +697,7 @@ func TestShardProcessor(t *testing.T) {
 				{
 					name: "should reject item on queue add failure",
 					setupHarness: func(h *testHarness) {
-						mockQueue := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+						mockQueue := h.addQueue(testFlow)
 						mockQueue.AddFunc = func(types.QueueItemAccessor) error { return testErr }
 					},
 					assert: func(t *testing.T, h *testHarness, item *flowItem) {
@@ -721,7 +710,7 @@ func TestShardProcessor(t *testing.T) {
 				{
 					name: "should ignore an already-finalized item",
 					setupHarness: func(h *testHarness) {
-						mockQueue := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
+						mockQueue := h.addQueue(testFlow)
 						var addCallCount int
 						mockQueue.AddFunc = func(item types.QueueItemAccessor) error {
 							addCallCount++
@@ -734,7 +723,7 @@ func TestShardProcessor(t *testing.T) {
 					},
 					item: func() *flowItem {
 						// Create a pre-finalized item.
-						item := newTestHarness(t, 0).newTestItem("req-finalized", testFlow.ID, testTTL)
+						item := newTestHarness(t, 0).newTestItem("req-finalized", testFlow, testTTL)
 						item.finalize(types.QueueOutcomeDispatched, nil)
 						return item
 					}(),
@@ -754,7 +743,7 @@ func TestShardProcessor(t *testing.T) {
 					tc.setupHarness(h)
 					item := tc.item
 					if item == nil {
-						item = h.newTestItem("req-enqueue-test", testFlow.ID, testTTL)
+						item = h.newTestItem("req-enqueue-test", testFlow, testTTL)
 					}
 					h.processor.enqueue(item)
 					tc.assert(t, h, item)
@@ -833,7 +822,6 @@ func TestShardProcessor(t *testing.T) {
 				t.Parallel()
 				policyErr := errors.New("policy failure")
 				registryErr := errors.New("registry error")
-				specA := types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority}
 
 				testCases := []struct {
 					name              string
@@ -843,7 +831,7 @@ func TestShardProcessor(t *testing.T) {
 					{
 						name: "should do nothing if no items are queued",
 						setupHarness: func(h *testHarness) {
-							h.addQueue(specA) // Add a queue, but no items.
+							h.addQueue(testFlow) // Add a queue, but no items.
 						},
 						expectDidDispatch: false,
 					},
@@ -851,13 +839,13 @@ func TestShardProcessor(t *testing.T) {
 						name: "should stop dispatching when filter signals pause",
 						setupHarness: func(h *testHarness) {
 							// Add an item that *could* be dispatched to prove the pause is effective.
-							q := h.addQueue(types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority})
-							require.NoError(t, q.Add(h.newTestItem("item", testFlow.ID, testTTL)))
+							q := h.addQueue(testFlow)
+							require.NoError(t, q.Add(h.newTestItem("item", testFlow, testTTL)))
 							h.processor.dispatchFilter = func(
 								_ context.Context,
 								_ framework.PriorityBandAccessor,
 								_ logr.Logger,
-							) (map[string]struct{}, bool) {
+							) (framework.PriorityBandAccessor, bool) {
 								return nil, true // Signal pause.
 							}
 						},
@@ -875,7 +863,7 @@ func TestShardProcessor(t *testing.T) {
 					{
 						name: "should skip band on inter-flow policy error",
 						setupHarness: func(h *testHarness) {
-							h.addQueue(specA)
+							h.addQueue(testFlow)
 							h.interFlowPolicySelectQueue = func(
 								_ framework.PriorityBandAccessor,
 							) (framework.FlowQueueAccessor, error) {
@@ -887,8 +875,8 @@ func TestShardProcessor(t *testing.T) {
 					{
 						name: "should skip band if inter-flow policy returns no queue",
 						setupHarness: func(h *testHarness) {
-							q := h.addQueue(specA)
-							require.NoError(t, q.Add(h.newTestItem("item", testFlow.ID, testTTL)))
+							q := h.addQueue(testFlow)
+							require.NoError(t, q.Add(h.newTestItem("item", testFlow, testTTL)))
 							h.interFlowPolicySelectQueue = func(
 								_ framework.PriorityBandAccessor,
 							) (framework.FlowQueueAccessor, error) {
@@ -900,8 +888,8 @@ func TestShardProcessor(t *testing.T) {
 					{
 						name: "should skip band on intra-flow policy error",
 						setupHarness: func(h *testHarness) {
-							q := h.addQueue(specA)
-							require.NoError(t, q.Add(h.newTestItem("item", testFlow.ID, testTTL)))
+							q := h.addQueue(testFlow)
+							require.NoError(t, q.Add(h.newTestItem("item", testFlow, testTTL)))
 							h.interFlowPolicySelectQueue = func(
 								_ framework.PriorityBandAccessor,
 							) (framework.FlowQueueAccessor, error) {
@@ -916,8 +904,8 @@ func TestShardProcessor(t *testing.T) {
 					{
 						name: "should skip band if intra-flow policy returns no item",
 						setupHarness: func(h *testHarness) {
-							q := h.addQueue(specA)
-							require.NoError(t, q.Add(h.newTestItem("item", testFlow.ID, testTTL)))
+							q := h.addQueue(testFlow)
+							require.NoError(t, q.Add(h.newTestItem("item", testFlow, testTTL)))
 							h.interFlowPolicySelectQueue = func(
 								_ framework.PriorityBandAccessor,
 							) (framework.FlowQueueAccessor, error) {
@@ -933,12 +921,12 @@ func TestShardProcessor(t *testing.T) {
 						name: "should continue to lower priority band on inter-flow policy error",
 						setupHarness: func(h *testHarness) {
 							// Create a failing high-priority queue and a working low-priority queue.
-							specHigh := types.FlowSpecification{ID: "flow-high", Priority: testFlow.Priority}
-							specLow := types.FlowSpecification{ID: "flow-low", Priority: 20}
-							h.addQueue(specHigh)
-							qLow := h.addQueue(specLow)
+							keyHigh := types.FlowKey{ID: "flow-high", Priority: testFlow.Priority}
+							keyLow := types.FlowKey{ID: "flow-low", Priority: 20}
+							h.addQueue(keyHigh)
+							qLow := h.addQueue(keyLow)
 
-							itemLow := h.newTestItem("item-low", specLow.ID, testTTL)
+							itemLow := h.newTestItem("item-low", keyLow, testTTL)
 							require.NoError(t, qLow.Add(itemLow))
 
 							h.interFlowPolicySelectQueue = func(
@@ -948,7 +936,7 @@ func TestShardProcessor(t *testing.T) {
 									return nil, errors.New("policy failure") // Fail high-priority.
 								}
 								// Succeed for low-priority.
-								q, _ := h.managedQueue(specLow.ID, specLow.Priority)
+								q, _ := h.managedQueue(keyLow)
 								return q.FlowQueueAccessor(), nil
 							}
 						},
@@ -971,34 +959,33 @@ func TestShardProcessor(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				specA := types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority}
-				specB := types.FlowSpecification{ID: "flow-b", Priority: testFlow.Priority}
-				h.addQueue(specA)
-				qB := h.addQueue(specB)
-				itemB := h.newTestItem("item-b", specB.ID, testTTL)
+				flowB := types.FlowKey{ID: "flow-b", Priority: testFlow.Priority}
+				h.addQueue(testFlow)
+				qB := h.addQueue(flowB)
+				itemB := h.newTestItem("item-b", flowB, testTTL)
 				require.NoError(t, qB.Add(itemB))
 
 				// This filter only allows flow-b.
 				h.processor.dispatchFilter = func(
 					_ context.Context,
-					_ framework.PriorityBandAccessor,
+					originalBand framework.PriorityBandAccessor,
 					_ logr.Logger,
-				) (map[string]struct{}, bool) {
-					return map[string]struct{}{specB.ID: {}}, false
+				) (framework.PriorityBandAccessor, bool) {
+					return newSubsetPriorityBandAccessor(originalBand, []types.FlowKey{flowB}), false
 				}
 
 				// This policy will be given the filtered view, so it should only see flow-b.
 				h.interFlowPolicySelectQueue = func(band framework.PriorityBandAccessor) (framework.FlowQueueAccessor, error) {
 					var flowIDs []string
 					band.IterateQueues(func(fqa framework.FlowQueueAccessor) bool {
-						flowIDs = append(flowIDs, fqa.FlowSpec().ID)
+						flowIDs = append(flowIDs, fqa.FlowKey().ID)
 						return true
 					})
 					// This is the core assertion of the test.
-					require.ElementsMatch(t, []string{specB.ID}, flowIDs, "Policy should only see the filtered flow")
+					require.ElementsMatch(t, []string{flowB.ID}, flowIDs, "Policy should only see the filtered flow")
 
 					// Select flow-b to prove the chain works.
-					q, _ := h.managedQueue(specB.ID, specB.Priority)
+					q, _ := h.managedQueue(flowB)
 					return q.FlowQueueAccessor(), nil
 				}
 
@@ -1016,22 +1003,22 @@ func TestShardProcessor(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				specHigh := types.FlowSpecification{ID: "flow-high", Priority: 10}
-				specLow := types.FlowSpecification{ID: "flow-low", Priority: 20}
-				qHigh := h.addQueue(specHigh)
-				qLow := h.addQueue(specLow)
+				keyHigh := types.FlowKey{ID: "flow-high", Priority: 10}
+				keyLow := types.FlowKey{ID: "flow-low", Priority: 20}
+				qHigh := h.addQueue(keyHigh)
+				qLow := h.addQueue(keyLow)
 
 				const numItems = 3
 				highPrioItems := make([]*flowItem, numItems)
 				lowPrioItems := make([]*flowItem, numItems)
 				for i := range numItems {
 					// Add high priority items.
-					itemH := h.newTestItem(fmt.Sprintf("req-high-%d", i), specHigh.ID, testTTL)
+					itemH := h.newTestItem(fmt.Sprintf("req-high-%d", i), keyHigh, testTTL)
 					require.NoError(t, qHigh.Add(itemH))
 					highPrioItems[i] = itemH
 
 					// Add low priority items.
-					itemL := h.newTestItem(fmt.Sprintf("req-low-%d", i), specLow.ID, testTTL)
+					itemL := h.newTestItem(fmt.Sprintf("req-low-%d", i), keyLow, testTTL)
 					require.NoError(t, qLow.Add(itemL))
 					lowPrioItems[i] = itemL
 				}
@@ -1075,14 +1062,14 @@ func TestShardProcessor(t *testing.T) {
 					{
 						name: "on ManagedQueue lookup failure",
 						setupMocks: func(h *testHarness) {
-							h.ManagedQueueFunc = func(string, uint) (contracts.ManagedQueue, error) { return nil, registryErr }
+							h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) { return nil, registryErr }
 						},
 						expectedErr: registryErr,
 					},
 					{
 						name: "on queue remove failure",
 						setupMocks: func(h *testHarness) {
-							h.ManagedQueueFunc = func(string, uint) (contracts.ManagedQueue, error) {
+							h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) {
 								return &mocks.MockManagedQueue{
 									RemoveFunc: func(types.QueueItemHandle) (types.QueueItemAccessor, error) {
 										return nil, registryErr
@@ -1099,8 +1086,8 @@ func TestShardProcessor(t *testing.T) {
 						t.Parallel()
 						h := newTestHarness(t, testCleanupTick)
 						tc.setupMocks(h)
-						item := h.newTestItem("req-dispatch-fail", testFlow.ID, testTTL)
-						err := h.processor.dispatchItem(item, testFlow.Priority, h.logger)
+						item := h.newTestItem("req-dispatch-fail", testFlow, testTTL)
+						err := h.processor.dispatchItem(item, h.logger)
 						require.Error(t, err, "dispatchItem should return an error")
 						assert.ErrorIs(t, err, tc.expectedErr, "The underlying registry error should be preserved")
 					})
@@ -1111,9 +1098,9 @@ func TestShardProcessor(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				item := h.newTestItem("req-expired-dispatch", testFlow.ID, testShortTTL)
+				item := h.newTestItem("req-expired-dispatch", testFlow, testShortTTL)
 
-				h.ManagedQueueFunc = func(string, uint) (contracts.ManagedQueue, error) {
+				h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) {
 					return &mocks.MockManagedQueue{
 						RemoveFunc: func(types.QueueItemHandle) (types.QueueItemAccessor, error) {
 							return item, nil
@@ -1123,7 +1110,7 @@ func TestShardProcessor(t *testing.T) {
 
 				// --- ACT ---
 				h.mockClock.Advance(testShortTTL * 2) // Make the item expire.
-				err := h.processor.dispatchItem(item, testFlow.Priority, h.logger)
+				err := h.processor.dispatchItem(item, h.logger)
 
 				// --- ASSERT ---
 				// First, check the error returned by `dispatchItem`.
@@ -1142,10 +1129,10 @@ func TestShardProcessor(t *testing.T) {
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
 				badItem := &typesmocks.MockQueueItemAccessor{
-					OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "bad-item", "", context.Background()),
+					OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "bad-item", testFlow, context.Background()),
 				}
 
-				h.ManagedQueueFunc = func(string, uint) (contracts.ManagedQueue, error) {
+				h.ManagedQueueFunc = func(types.FlowKey) (contracts.ManagedQueue, error) {
 					return &mocks.MockManagedQueue{
 						RemoveFunc: func(types.QueueItemHandle) (types.QueueItemAccessor, error) {
 							return badItem, nil
@@ -1153,13 +1140,13 @@ func TestShardProcessor(t *testing.T) {
 					}, nil
 				}
 
-				itemToDispatch := h.newTestItem("req-dispatch-panic", testFlow.ID, testTTL)
+				itemToDispatch := h.newTestItem("req-dispatch-panic", testFlow, testTTL)
 				expectedPanicMsg := fmt.Sprintf("%s: internal error: item %q of type %T is not a *flowItem",
 					errIntraFlow, "bad-item", badItem)
 
 				// --- ACT & ASSERT ---
 				assert.PanicsWithError(t, expectedPanicMsg, func() {
-					_ = h.processor.dispatchItem(itemToDispatch, testFlow.Priority, h.logger)
+					_ = h.processor.dispatchItem(itemToDispatch, h.logger)
 				}, "A type mismatch from a queue should cause a panic")
 			})
 		})
@@ -1171,10 +1158,9 @@ func TestShardProcessor(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				spec := types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority}
 				// Create an item that is already expired relative to the cleanup time.
-				item := h.newTestItem("req-expired", testFlow.ID, 1*time.Millisecond)
-				q := h.addQueue(spec)
+				item := h.newTestItem("req-expired", testFlow, 1*time.Millisecond)
+				q := h.addQueue(testFlow)
 				require.NoError(t, q.Add(item))
 				cleanupTime := h.mockClock.Now().Add(10 * time.Millisecond)
 
@@ -1192,9 +1178,8 @@ func TestShardProcessor(t *testing.T) {
 				t.Parallel()
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
-				spec := types.FlowSpecification{ID: testFlow.ID, Priority: testFlow.Priority}
-				item := h.newTestItem("req-pending", testFlow.ID, testTTL)
-				q := h.addQueue(spec)
+				item := h.newTestItem("req-pending", testFlow, testTTL)
+				q := h.addQueue(testFlow)
 				require.NoError(t, q.Add(item))
 
 				// --- ACT ---
@@ -1228,7 +1213,7 @@ func TestShardProcessor(t *testing.T) {
 				// --- ARRANGE ---
 				h := newTestHarness(t, testCleanupTick)
 				item := &typesmocks.MockQueueItemAccessor{
-					OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "bad-item", testFlow.ID, context.Background()),
+					OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "bad-item", testFlow, context.Background()),
 				}
 				items := []types.QueueItemAccessor{item}
 
@@ -1252,11 +1237,11 @@ func TestShardProcessor(t *testing.T) {
 				var processedCount atomic.Int32
 
 				for i := range numQueues {
-					spec := types.FlowSpecification{
+					key := types.FlowKey{
 						ID:       fmt.Sprintf("flow-%d", i),
 						Priority: testFlow.Priority,
 					}
-					h.addQueue(spec)
+					h.addQueue(key)
 				}
 
 				processFn := func(mq contracts.ManagedQueue, logger logr.Logger) {
@@ -1293,7 +1278,7 @@ func TestCheckItemExpiry(t *testing.T) {
 		{
 			name: "should not be expired if TTL is not reached and context is active",
 			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-not-expired", "", context.Background()),
+				typesmocks.NewMockFlowControlRequest(100, "req-not-expired", testFlow, context.Background()),
 				testTTL,
 				now),
 			now:           now.Add(30 * time.Second),
@@ -1304,7 +1289,7 @@ func TestCheckItemExpiry(t *testing.T) {
 		{
 			name: "should not be expired if TTL is disabled (0)",
 			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-not-expired-no-ttl", "", context.Background()),
+				typesmocks.NewMockFlowControlRequest(100, "req-not-expired-no-ttl", testFlow, context.Background()),
 				0,
 				now),
 			now:           now.Add(30 * time.Second),
@@ -1315,7 +1300,7 @@ func TestCheckItemExpiry(t *testing.T) {
 		{
 			name: "should be expired if TTL is exceeded",
 			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-ttl-expired", "", context.Background()),
+				typesmocks.NewMockFlowControlRequest(100, "req-ttl-expired", testFlow, context.Background()),
 				time.Second,
 				now),
 			now:           now.Add(2 * time.Second),
@@ -1326,7 +1311,7 @@ func TestCheckItemExpiry(t *testing.T) {
 		{
 			name: "should be expired if context is cancelled",
 			item: NewItem(
-				typesmocks.NewMockFlowControlRequest(100, "req-ctx-cancelled", "", ctxCancelled),
+				typesmocks.NewMockFlowControlRequest(100, "req-ctx-cancelled", testFlow, ctxCancelled),
 				testTTL,
 				now),
 			now:           now,
@@ -1337,7 +1322,10 @@ func TestCheckItemExpiry(t *testing.T) {
 		{
 			name: "should be expired if already finalized",
 			item: func() types.QueueItemAccessor {
-				i := NewItem(typesmocks.NewMockFlowControlRequest(100, "req-finalized", "", context.Background()), testTTL, now)
+				i := NewItem(
+					typesmocks.NewMockFlowControlRequest(100, "req-finalized", testFlow, context.Background()),
+					testTTL,
+					now)
 				i.finalize(types.QueueOutcomeDispatched, nil)
 				return i
 			}(),
@@ -1376,7 +1364,7 @@ func TestCheckItemExpiry(t *testing.T) {
 		t.Parallel()
 		// --- ARRANGE ---
 		badItem := &typesmocks.MockQueueItemAccessor{
-			OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "item-bad-type", "", context.Background()),
+			OriginalRequestV: typesmocks.NewMockFlowControlRequest(0, "item-bad-type", testFlow, context.Background()),
 		}
 
 		expectedPanicMsg := fmt.Sprintf("internal error: item %q of type %T is not a *flowItem",
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/besthead/besthead_test.go b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/besthead/besthead_test.go
index 4905a21..5971d3f 100644
--- a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/besthead/besthead_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/besthead/besthead_test.go
@@ -31,11 +31,16 @@ import (
 )
 
 const (
-	flow1           = "flow1"
-	flow2           = "flow2"
+	flow1ID         = "flow1"
+	flow2ID         = "flow2"
 	commonScoreType = "enqueue_time_ns_asc"
 )
 
+var (
+	flow1Key = types.FlowKey{ID: flow1ID, Priority: 0}
+	flow2Key = types.FlowKey{ID: flow2ID, Priority: 0}
+)
+
 // enqueueTimeComparatorFunc is a test utility. Lower enqueue time is better.
 func enqueueTimeComparatorFunc(a, b types.QueueItemAccessor) bool {
 	return a.EnqueueTime().Before(b.EnqueueTime())
@@ -49,20 +54,21 @@ func newTestComparator() *frameworkmocks.MockItemComparator {
 }
 
 func newTestBand(queues ...framework.FlowQueueAccessor) *frameworkmocks.MockPriorityBandAccessor {
-	flowIDs := make([]string, 0, len(queues))
+	flowKeys := make([]types.FlowKey, 0, len(queues))
 	queuesByID := make(map[string]framework.FlowQueueAccessor, len(queues))
 	for _, q := range queues {
-		flowIDs = append(flowIDs, q.FlowSpec().ID)
-		queuesByID[q.FlowSpec().ID] = q
+		key := q.FlowKey()
+		flowKeys = append(flowKeys, key)
+		queuesByID[key.ID] = q
 	}
 	return &frameworkmocks.MockPriorityBandAccessor{
-		FlowIDsFunc: func() []string { return flowIDs },
+		FlowKeysFunc: func() []types.FlowKey { return flowKeys },
 		QueueFunc: func(id string) framework.FlowQueueAccessor {
 			return queuesByID[id]
 		},
 		IterateQueuesFunc: func(iterator func(queue framework.FlowQueueAccessor) bool) {
-			for _, id := range flowIDs {
-				if !iterator(queuesByID[id]) {
+			for _, key := range flowKeys {
+				if !iterator(queuesByID[key.ID]) {
 					break
 				}
 			}
@@ -81,27 +87,27 @@ func TestBestHead_SelectQueue(t *testing.T) {
 	policy := newBestHead()
 	now := time.Now()
 
-	itemBetter := typesmocks.NewMockQueueItemAccessor(10, "itemBetter", flow1)
+	itemBetter := typesmocks.NewMockQueueItemAccessor(10, "itemBetter", flow1Key)
 	itemBetter.EnqueueTimeV = now.Add(-10 * time.Second)
-	itemWorse := typesmocks.NewMockQueueItemAccessor(20, "itemWorse", flow2)
+	itemWorse := typesmocks.NewMockQueueItemAccessor(20, "itemWorse", flow2Key)
 	itemWorse.EnqueueTimeV = now.Add(-5 * time.Second)
 
 	queue1 := &frameworkmocks.MockFlowQueueAccessor{
 		LenV:        1,
 		PeekHeadV:   itemBetter,
-		FlowSpecV:   types.FlowSpecification{ID: flow1},
+		FlowKeyV:    flow1Key,
 		ComparatorV: newTestComparator(),
 	}
 	queue2 := &frameworkmocks.MockFlowQueueAccessor{
 		LenV:        1,
 		PeekHeadV:   itemWorse,
-		FlowSpecV:   types.FlowSpecification{ID: flow2},
+		FlowKeyV:    flow2Key,
 		ComparatorV: newTestComparator(),
 	}
 	queueEmpty := &frameworkmocks.MockFlowQueueAccessor{
 		LenV:         0,
 		PeekHeadErrV: framework.ErrQueueEmpty,
-		FlowSpecV:    types.FlowSpecification{ID: "flowEmpty"},
+		FlowKeyV:     types.FlowKey{ID: "flowEmpty"},
 		ComparatorV:  newTestComparator(),
 	}
 
@@ -115,17 +121,17 @@ func TestBestHead_SelectQueue(t *testing.T) {
 		{
 			name:            "BasicSelection_TwoQueues",
 			band:            newTestBand(queue1, queue2),
-			expectedQueueID: flow1,
+			expectedQueueID: flow1ID,
 		},
 		{
 			name:            "IgnoresEmptyQueues",
 			band:            newTestBand(queue1, queueEmpty, queue2),
-			expectedQueueID: flow1,
+			expectedQueueID: flow1ID,
 		},
 		{
 			name:            "SingleNonEmptyQueue",
 			band:            newTestBand(queue1),
-			expectedQueueID: flow1,
+			expectedQueueID: flow1ID,
 		},
 		{
 			name: "ComparatorCompatibility",
@@ -133,13 +139,13 @@ func TestBestHead_SelectQueue(t *testing.T) {
 				&frameworkmocks.MockFlowQueueAccessor{
 					LenV:        1,
 					PeekHeadV:   itemBetter,
-					FlowSpecV:   types.FlowSpecification{ID: flow1},
+					FlowKeyV:    flow1Key,
 					ComparatorV: &frameworkmocks.MockItemComparator{ScoreTypeV: "typeA", FuncV: enqueueTimeComparatorFunc},
 				},
 				&frameworkmocks.MockFlowQueueAccessor{
 					LenV:        1,
 					PeekHeadV:   itemWorse,
-					FlowSpecV:   types.FlowSpecification{ID: flow2},
+					FlowKeyV:    flow2Key,
 					ComparatorV: &frameworkmocks.MockItemComparator{ScoreTypeV: "typeB", FuncV: enqueueTimeComparatorFunc},
 				},
 			),
@@ -151,12 +157,12 @@ func TestBestHead_SelectQueue(t *testing.T) {
 				&frameworkmocks.MockFlowQueueAccessor{
 					LenV:         1,
 					PeekHeadErrV: errors.New("peek error"),
-					FlowSpecV:    types.FlowSpecification{ID: flow1},
+					FlowKeyV:     flow1Key,
 					ComparatorV:  newTestComparator(),
 				},
 				queue2,
 			),
-			expectedQueueID: flow2,
+			expectedQueueID: flow2ID,
 		},
 		{
 			name: "QueueComparatorIsNil",
@@ -164,7 +170,7 @@ func TestBestHead_SelectQueue(t *testing.T) {
 				&frameworkmocks.MockFlowQueueAccessor{
 					LenV:        1,
 					PeekHeadV:   itemBetter,
-					FlowSpecV:   types.FlowSpecification{ID: flow1},
+					FlowKeyV:    flow1Key,
 					ComparatorV: nil,
 				},
 				queue2,
@@ -177,7 +183,7 @@ func TestBestHead_SelectQueue(t *testing.T) {
 				&frameworkmocks.MockFlowQueueAccessor{
 					LenV:        1,
 					PeekHeadV:   itemBetter,
-					FlowSpecV:   types.FlowSpecification{ID: flow1},
+					FlowKeyV:    flow1Key,
 					ComparatorV: &frameworkmocks.MockItemComparator{ScoreTypeV: commonScoreType, FuncV: nil},
 				},
 				queue2,
@@ -191,7 +197,7 @@ func TestBestHead_SelectQueue(t *testing.T) {
 				&frameworkmocks.MockFlowQueueAccessor{
 					LenV:         0,
 					PeekHeadErrV: framework.ErrQueueEmpty,
-					FlowSpecV:    types.FlowSpecification{ID: "flowEmpty2"},
+					FlowKeyV:     types.FlowKey{ID: "flowEmpty2"},
 					ComparatorV:  newTestComparator(),
 				},
 			),
@@ -223,7 +229,7 @@ func TestBestHead_SelectQueue(t *testing.T) {
 					assert.Nil(t, selected, "No queue should be selected")
 				} else {
 					require.NotNil(t, selected, "A queue should have been selected")
-					assert.Equal(t, tc.expectedQueueID, selected.FlowSpec().ID, "The selected queue should have the expected ID")
+					assert.Equal(t, tc.expectedQueueID, selected.FlowKey().ID, "The selected queue should have the expected ID")
 				}
 			}
 		})
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/functional_test.go b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/functional_test.go
index 9dacc2a..7649b32 100644
--- a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/functional_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/functional_test.go
@@ -65,7 +65,7 @@ func runSelectQueueConformanceTests(t *testing.T, policy framework.InterFlowDisp
 	mockQueueEmpty := &frameworkmocks.MockFlowQueueAccessor{
 		LenV:         0,
 		PeekHeadErrV: framework.ErrQueueEmpty,
-		FlowSpecV:    types.FlowSpecification{ID: flowIDEmpty},
+		FlowKeyV:     types.FlowKey{ID: flowIDEmpty},
 	}
 
 	testCases := []struct {
@@ -84,7 +84,7 @@ func runSelectQueueConformanceTests(t *testing.T, policy framework.InterFlowDisp
 		{
 			name: "With an empty priority band accessor",
 			band: &frameworkmocks.MockPriorityBandAccessor{
-				FlowIDsFunc:       func() []string { return []string{} },
+				FlowKeysFunc:      func() []types.FlowKey { return []types.FlowKey{} },
 				IterateQueuesFunc: func(callback func(queue framework.FlowQueueAccessor) bool) { /* no-op */ },
 			},
 			expectErr: false,
@@ -93,7 +93,7 @@ func runSelectQueueConformanceTests(t *testing.T, policy framework.InterFlowDisp
 		{
 			name: "With a band that has one empty queue",
 			band: &frameworkmocks.MockPriorityBandAccessor{
-				FlowIDsFunc: func() []string { return []string{flowIDEmpty} },
+				FlowKeysFunc: func() []types.FlowKey { return []types.FlowKey{{ID: flowIDEmpty}} },
 				QueueFunc: func(fID string) framework.FlowQueueAccessor {
 					if fID == flowIDEmpty {
 						return mockQueueEmpty
@@ -107,7 +107,7 @@ func runSelectQueueConformanceTests(t *testing.T, policy framework.InterFlowDisp
 		{
 			name: "With a band that has multiple empty queues",
 			band: &frameworkmocks.MockPriorityBandAccessor{
-				FlowIDsFunc: func() []string { return []string{flowIDEmpty, "flow-empty-2"} },
+				FlowKeysFunc: func() []types.FlowKey { return []types.FlowKey{{ID: flowIDEmpty}, {ID: "flow-empty-2"}} },
 				QueueFunc: func(fID string) framework.FlowQueueAccessor {
 					return mockQueueEmpty
 				},
@@ -136,7 +136,7 @@ func runSelectQueueConformanceTests(t *testing.T, policy framework.InterFlowDisp
 			}
 
 			if tc.expectedQueue != nil {
-				assert.Equal(t, tc.expectedQueue.FlowSpec().ID, selectedQueue.FlowSpec().ID,
+				assert.Equal(t, tc.expectedQueue.FlowKey(), selectedQueue.FlowKey(),
 					"SelectQueue for policy %s returned an unexpected queue", policy.Name())
 			}
 		})
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin_test.go b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin_test.go
index f2255f0..dd4a026 100644
--- a/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/interflow/dispatch/roundrobin/roundrobin_test.go
@@ -30,6 +30,12 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types"
 )
 
+var (
+	flow1Key = types.FlowKey{ID: "flow1", Priority: 0}
+	flow2Key = types.FlowKey{ID: "flow2", Priority: 0}
+	flow3Key = types.FlowKey{ID: "flow3", Priority: 0}
+)
+
 func TestRoundRobin_Name(t *testing.T) {
 	t.Parallel()
 	policy := newRoundRobin()
@@ -41,12 +47,12 @@ func TestRoundRobin_SelectQueue_Logic(t *testing.T) {
 	policy := newRoundRobin()
 
 	// Setup: Three non-empty queues
-	queue1 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowSpecV: types.FlowSpecification{ID: "flow1"}}
-	queue2 := &frameworkmocks.MockFlowQueueAccessor{LenV: 2, FlowSpecV: types.FlowSpecification{ID: "flow2"}}
-	queue3 := &frameworkmocks.MockFlowQueueAccessor{LenV: 3, FlowSpecV: types.FlowSpecification{ID: "flow3"}}
+	queue1 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowKeyV: flow1Key}
+	queue2 := &frameworkmocks.MockFlowQueueAccessor{LenV: 2, FlowKeyV: flow2Key}
+	queue3 := &frameworkmocks.MockFlowQueueAccessor{LenV: 3, FlowKeyV: flow3Key}
 
 	mockBand := &frameworkmocks.MockPriorityBandAccessor{
-		FlowIDsFunc: func() []string { return []string{"flow3", "flow1", "flow2"} }, // Unsorted to test sorting
+		FlowKeysFunc: func() []types.FlowKey { return []types.FlowKey{flow3Key, flow1Key, flow2Key} }, // Unsorted to test sorting
 		QueueFunc: func(id string) framework.FlowQueueAccessor {
 			switch id {
 			case "flow1":
@@ -60,7 +66,7 @@ func TestRoundRobin_SelectQueue_Logic(t *testing.T) {
 		},
 	}
 
-	// Expected order is based on sorted FlowIDs: flow1, flow2, flow3
+	// Expected order is based on sorted FlowKeys: flow1, flow2, flow3
 	expectedOrder := []string{"flow1", "flow2", "flow3"}
 
 	// First cycle
@@ -68,7 +74,7 @@ func TestRoundRobin_SelectQueue_Logic(t *testing.T) {
 		selected, err := policy.SelectQueue(mockBand)
 		require.NoError(t, err, "SelectQueue should not error on a valid band")
 		require.NotNil(t, selected, "SelectQueue should have selected a queue")
-		assert.Equal(t, expectedOrder[i], selected.FlowSpec().ID,
+		assert.Equal(t, expectedOrder[i], selected.FlowKey().ID,
 			"Cycle 1, selection %d should be %s", i+1, expectedOrder[i])
 	}
 
@@ -77,7 +83,7 @@ func TestRoundRobin_SelectQueue_Logic(t *testing.T) {
 		selected, err := policy.SelectQueue(mockBand)
 		require.NoError(t, err, "SelectQueue should not error on a valid band")
 		require.NotNil(t, selected, "SelectQueue should have selected a queue")
-		assert.Equal(t, expectedOrder[i], selected.FlowSpec().ID,
+		assert.Equal(t, expectedOrder[i], selected.FlowKey().ID,
 			"Cycle 2, selection %d should be %s", i+1, expectedOrder[i])
 	}
 }
@@ -87,12 +93,13 @@ func TestRoundRobin_SelectQueue_SkipsEmptyQueues(t *testing.T) {
 	policy := newRoundRobin()
 
 	// Setup: Two non-empty queues and one empty queue
-	queue1 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowSpecV: types.FlowSpecification{ID: "flow1"}}
-	queueEmpty := &frameworkmocks.MockFlowQueueAccessor{LenV: 0, FlowSpecV: types.FlowSpecification{ID: "flowEmpty"}}
-	queue3 := &frameworkmocks.MockFlowQueueAccessor{LenV: 3, FlowSpecV: types.FlowSpecification{ID: "flow3"}}
+	flowEmptyKey := types.FlowKey{ID: "flowEmpty", Priority: 0}
+	queue1 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowKeyV: flow1Key}
+	queueEmpty := &frameworkmocks.MockFlowQueueAccessor{LenV: 0, FlowKeyV: flowEmptyKey}
+	queue3 := &frameworkmocks.MockFlowQueueAccessor{LenV: 3, FlowKeyV: flow3Key}
 
 	mockBand := &frameworkmocks.MockPriorityBandAccessor{
-		FlowIDsFunc: func() []string { return []string{"flow1", "flowEmpty", "flow3"} },
+		FlowKeysFunc: func() []types.FlowKey { return []types.FlowKey{flow1Key, flowEmptyKey, flow3Key} },
 		QueueFunc: func(id string) framework.FlowQueueAccessor {
 			switch id {
 			case "flow1":
@@ -110,17 +117,17 @@ func TestRoundRobin_SelectQueue_SkipsEmptyQueues(t *testing.T) {
 	selected, err := policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error when skipping queues")
 	require.NotNil(t, selected, "SelectQueue should select the first non-empty queue")
-	assert.Equal(t, "flow1", selected.FlowSpec().ID, "First selection should be flow1")
+	assert.Equal(t, "flow1", selected.FlowKey().ID, "First selection should be flow1")
 
 	selected, err = policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error when skipping queues")
 	require.NotNil(t, selected, "SelectQueue should select the second non-empty queue")
-	assert.Equal(t, "flow3", selected.FlowSpec().ID, "Second selection should be flow3, skipping flowEmpty")
+	assert.Equal(t, "flow3", selected.FlowKey().ID, "Second selection should be flow3, skipping flowEmpty")
 
 	selected, err = policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error when wrapping around")
 	require.NotNil(t, selected, "SelectQueue should wrap around and select a queue")
-	assert.Equal(t, "flow1", selected.FlowSpec().ID, "Should wrap around and select flow1 again")
+	assert.Equal(t, "flow1", selected.FlowKey().ID, "Should wrap around and select flow1 again")
 }
 
 func TestRoundRobin_SelectQueue_HandlesDynamicFlows(t *testing.T) {
@@ -128,10 +135,10 @@ func TestRoundRobin_SelectQueue_HandlesDynamicFlows(t *testing.T) {
 	policy := newRoundRobin()
 
 	// Initial setup
-	queue1 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowSpecV: types.FlowSpecification{ID: "flow1"}}
-	queue2 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowSpecV: types.FlowSpecification{ID: "flow2"}}
+	queue1 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowKeyV: flow1Key}
+	queue2 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowKeyV: flow2Key}
 	mockBand := &frameworkmocks.MockPriorityBandAccessor{
-		FlowIDsFunc: func() []string { return []string{"flow1", "flow2"} },
+		FlowKeysFunc: func() []types.FlowKey { return []types.FlowKey{flow1Key, flow2Key} },
 		QueueFunc: func(id string) framework.FlowQueueAccessor {
 			if id == "flow1" {
 				return queue1
@@ -144,11 +151,11 @@ func TestRoundRobin_SelectQueue_HandlesDynamicFlows(t *testing.T) {
 	selected, err := policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error on initial selection")
 	require.NotNil(t, selected, "SelectQueue should select a queue initially")
-	assert.Equal(t, "flow1", selected.FlowSpec().ID, "First selection should be flow1")
+	assert.Equal(t, "flow1", selected.FlowKey().ID, "First selection should be flow1")
 
 	// --- Simulate adding a flow ---
-	queue3 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowSpecV: types.FlowSpecification{ID: "flow3"}}
-	mockBand.FlowIDsFunc = func() []string { return []string{"flow1", "flow2", "flow3"} }
+	queue3 := &frameworkmocks.MockFlowQueueAccessor{LenV: 1, FlowKeyV: flow3Key}
+	mockBand.FlowKeysFunc = func() []types.FlowKey { return []types.FlowKey{flow1Key, flow2Key, flow3Key} }
 	mockBand.QueueFunc = func(id string) framework.FlowQueueAccessor {
 		switch id {
 		case "flow1":
@@ -165,22 +172,22 @@ func TestRoundRobin_SelectQueue_HandlesDynamicFlows(t *testing.T) {
 	selected, err = policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error after adding a flow")
 	require.NotNil(t, selected, "SelectQueue should select a queue after adding a flow")
-	assert.Equal(t, "flow2", selected.FlowSpec().ID, "Next selection should be flow2")
+	assert.Equal(t, "flow2", selected.FlowKey().ID, "Next selection should be flow2")
 
 	// Next selection should be flow3
 	selected, err = policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error on the third selection")
 	require.NotNil(t, selected, "SelectQueue should select the new flow")
-	assert.Equal(t, "flow3", selected.FlowSpec().ID, "Next selection should be the new flow3")
+	assert.Equal(t, "flow3", selected.FlowKey().ID, "Next selection should be the new flow3")
 
 	// --- Simulate removing a flow ---
-	mockBand.FlowIDsFunc = func() []string { return []string{"flow1", "flow3"} } // flow2 is removed
+	mockBand.FlowKeysFunc = func() []types.FlowKey { return []types.FlowKey{flow1Key, flow3Key} } // flow2 is removed
 
 	// Next selection should wrap around and pick flow1
 	selected, err = policy.SelectQueue(mockBand)
 	require.NoError(t, err, "SelectQueue should not error after removing a flow")
 	require.NotNil(t, selected, "SelectQueue should select a queue after removing a flow")
-	assert.Equal(t, "flow1", selected.FlowSpec().ID, "Next selection should wrap around to flow1 after a removal")
+	assert.Equal(t, "flow1", selected.FlowKey().ID, "Next selection should wrap around to flow1 after a removal")
 }
 
 func TestRoundRobin_SelectQueue_Concurrency(t *testing.T) {
@@ -193,17 +200,17 @@ func TestRoundRobin_SelectQueue_Concurrency(t *testing.T) {
 
 			// Setup: Three non-empty queues
 			queues := []*frameworkmocks.MockFlowQueueAccessor{
-				{LenV: 1, FlowSpecV: types.FlowSpecification{ID: "flow1"}},
-				{LenV: 2, FlowSpecV: types.FlowSpecification{ID: "flow2"}},
-				{LenV: 3, FlowSpecV: types.FlowSpecification{ID: "flow3"}},
+				{LenV: 1, FlowKeyV: flow1Key},
+				{LenV: 2, FlowKeyV: flow2Key},
+				{LenV: 3, FlowKeyV: flow3Key},
 			}
 			numQueues := int64(len(queues))
 
 			mockBand := &frameworkmocks.MockPriorityBandAccessor{
-				FlowIDsFunc: func() []string { return []string{"flow1", "flow2", "flow3"} },
+				FlowKeysFunc: func() []types.FlowKey { return []types.FlowKey{flow1Key, flow2Key, flow3Key} },
 				QueueFunc: func(id string) framework.FlowQueueAccessor {
 					for _, q := range queues {
-						if q.FlowSpec().ID == id {
+						if q.FlowKey().ID == id {
 							return q
 						}
 					}
@@ -225,7 +232,7 @@ func TestRoundRobin_SelectQueue_Concurrency(t *testing.T) {
 					for range selectionsPerGoroutine {
 						selected, err := policy.SelectQueue(mockBand)
 						if err == nil && selected != nil {
-							val, _ := selectionCounts.LoadOrStore(selected.FlowSpec().ID, new(atomic.Int64))
+							val, _ := selectionCounts.LoadOrStore(selected.FlowKey().ID, new(atomic.Int64))
 							val.(*atomic.Int64).Add(1)
 						}
 					}
diff --git a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go
index 8a13c6c..45c1442 100644
--- a/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/policies/intraflow/dispatch/fcfs/fcfs_test.go
@@ -29,6 +29,8 @@ import (
 	typesmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
 )
 
+var testFlowKey = types.FlowKey{ID: "test-flow", Priority: 0}
+
 func TestFCFS_Name(t *testing.T) {
 	t.Parallel()
 	policy := newFCFS()
@@ -49,7 +51,7 @@ func TestFCFS_SelectItem(t *testing.T) {
 	// This unit test focuses on the policy-specific success path.
 	policy := newFCFS()
 
-	mockItem := typesmocks.NewMockQueueItemAccessor(1, "item1", "flow1")
+	mockItem := typesmocks.NewMockQueueItemAccessor(1, "item1", testFlowKey)
 	mockQueue := &frameworkmocks.MockFlowQueueAccessor{
 		PeekHeadV: mockItem,
 		LenV:      1,
@@ -67,13 +69,13 @@ func TestEnqueueTimeComparator_Func(t *testing.T) {
 	require.NotNil(t, compareFunc)
 
 	now := time.Now()
-	itemA := typesmocks.NewMockQueueItemAccessor(10, "itemA", "test-flow")
+	itemA := typesmocks.NewMockQueueItemAccessor(10, "itemA", testFlowKey)
 	itemA.EnqueueTimeV = now
 
-	itemB := typesmocks.NewMockQueueItemAccessor(20, "itemB", "test-flow")
+	itemB := typesmocks.NewMockQueueItemAccessor(20, "itemB", testFlowKey)
 	itemB.EnqueueTimeV = now.Add(time.Second) // B is later than A
 
-	itemC := typesmocks.NewMockQueueItemAccessor(30, "itemC", "test-flow")
+	itemC := typesmocks.NewMockQueueItemAccessor(30, "itemC", testFlowKey)
 	itemC.EnqueueTimeV = now // C is same time as A
 
 	testCases := []struct {
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go b/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go
index 916cacd..7d20270 100644
--- a/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go
@@ -27,6 +27,8 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
 )
 
+var benchmarkFlowKey = types.FlowKey{ID: "benchmark-flow"}
+
 // BenchmarkQueues runs a series of benchmarks against all registered queue implementations.
 func BenchmarkQueues(b *testing.B) {
 	for queueName, constructor := range queue.RegisteredQueues {
@@ -68,7 +70,7 @@ func benchmarkAddRemove(b *testing.B, q framework.SafeQueue) {
 
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+			item := mocks.NewMockQueueItemAccessor(1, "item", benchmarkFlowKey)
 			err := q.Add(item)
 			if err != nil {
 				b.Fatalf("Add failed: %v", err)
@@ -85,7 +87,7 @@ func benchmarkAddRemove(b *testing.B, q framework.SafeQueue) {
 // common consumer pattern where a single worker peeks at an item before deciding to process and remove it.
 func benchmarkAddPeekRemove(b *testing.B, q framework.SafeQueue) {
 	// Pre-add one item so PeekHead doesn't fail on the first iteration.
-	initialItem := mocks.NewMockQueueItemAccessor(1, "initial", "benchmark-flow")
+	initialItem := mocks.NewMockQueueItemAccessor(1, "initial", benchmarkFlowKey)
 	if err := q.Add(initialItem); err != nil {
 		b.Fatalf("Failed to add initial item: %v", err)
 	}
@@ -93,7 +95,7 @@ func benchmarkAddPeekRemove(b *testing.B, q framework.SafeQueue) {
 	b.ReportAllocs()
 
 	for i := 0; i < b.N; i++ {
-		item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+		item := mocks.NewMockQueueItemAccessor(1, "item", benchmarkFlowKey)
 		err := q.Add(item)
 		if err != nil {
 			b.Fatalf("Add failed: %v", err)
@@ -122,7 +124,7 @@ func benchmarkBulkAddThenBulkRemove(b *testing.B, q framework.SafeQueue) {
 		// Add a batch of items
 		items := make([]types.QueueItemAccessor, 100)
 		for j := range items {
-			item := mocks.NewMockQueueItemAccessor(1, fmt.Sprintf("bulk-%d-%d", i, j), "benchmark-flow")
+			item := mocks.NewMockQueueItemAccessor(1, fmt.Sprintf("bulk-%d-%d", i, j), benchmarkFlowKey)
 			items[j] = item
 			if err := q.Add(item); err != nil {
 				b.Fatalf("Add failed: %v", err)
@@ -146,7 +148,7 @@ func benchmarkBulkAddThenBulkRemove(b *testing.B, q framework.SafeQueue) {
 // understanding the performance of accessing the lowest-priority item.
 func benchmarkAddPeekTailRemove(b *testing.B, q framework.SafeQueue) {
 	// Pre-add one item so PeekTail doesn't fail on the first iteration.
-	initialItem := mocks.NewMockQueueItemAccessor(1, "initial", "benchmark-flow")
+	initialItem := mocks.NewMockQueueItemAccessor(1, "initial", benchmarkFlowKey)
 	if err := q.Add(initialItem); err != nil {
 		b.Fatalf("Failed to add initial item: %v", err)
 	}
@@ -154,7 +156,7 @@ func benchmarkAddPeekTailRemove(b *testing.B, q framework.SafeQueue) {
 	b.ReportAllocs()
 
 	for i := 0; i < b.N; i++ {
-		item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+		item := mocks.NewMockQueueItemAccessor(1, "item", benchmarkFlowKey)
 		err := q.Add(item)
 		if err != nil {
 			b.Fatalf("Add failed: %v", err)
@@ -177,7 +179,7 @@ func benchmarkAddPeekTailRemove(b *testing.B, q framework.SafeQueue) {
 func benchmarkHighContention(b *testing.B, q framework.SafeQueue) {
 	// Pre-fill the queue to ensure consumers have work to do immediately.
 	for i := range 1000 {
-		item := mocks.NewMockQueueItemAccessor(1, fmt.Sprintf("prefill-%d", i), "benchmark-flow")
+		item := mocks.NewMockQueueItemAccessor(1, fmt.Sprintf("prefill-%d", i), benchmarkFlowKey)
 		if err := q.Add(item); err != nil {
 			b.Fatalf("Failed to pre-fill queue: %v", err)
 		}
@@ -196,7 +198,7 @@ func benchmarkHighContention(b *testing.B, q framework.SafeQueue) {
 				case <-stopCh:
 					return
 				default:
-					item := mocks.NewMockQueueItemAccessor(1, "item", "benchmark-flow")
+					item := mocks.NewMockQueueItemAccessor(1, "item", benchmarkFlowKey)
 					_ = q.Add(item)
 				}
 			}
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go b/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go
index fe01053..d6ff8a2 100644
--- a/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go
@@ -184,7 +184,7 @@ func TestQueueConformance(t *testing.T) {
 	for queueName, constructor := range queue.RegisteredQueues {
 		t.Run(string(queueName), func(t *testing.T) {
 			t.Parallel()
-			flowSpec := &types.FlowSpecification{ID: "test-flow-1", Priority: 0}
+			flowKey := types.FlowKey{ID: "test-flow-1", Priority: 0}
 
 			t.Run("Initialization", func(t *testing.T) {
 				t.Parallel()
@@ -206,11 +206,11 @@ func TestQueueConformance(t *testing.T) {
 
 				now := time.Now()
 
-				item1 := typesmocks.NewMockQueueItemAccessor(100, "item1_fifo", flowSpec.ID)
+				item1 := typesmocks.NewMockQueueItemAccessor(100, "item1_fifo", flowKey)
 				item1.EnqueueTimeV = now.Add(-2 * time.Second) // Earliest
-				item2 := typesmocks.NewMockQueueItemAccessor(50, "item2_fifo", flowSpec.ID)
+				item2 := typesmocks.NewMockQueueItemAccessor(50, "item2_fifo", flowKey)
 				item2.EnqueueTimeV = now.Add(-1 * time.Second) // Middle
-				item3 := typesmocks.NewMockQueueItemAccessor(20, "item3_fifo", flowSpec.ID)
+				item3 := typesmocks.NewMockQueueItemAccessor(20, "item3_fifo", flowKey)
 				item3.EnqueueTimeV = now // Latest
 
 				itemsInFIFOOrder := []*typesmocks.MockQueueItemAccessor{item1, item2, item3}
@@ -224,9 +224,9 @@ func TestQueueConformance(t *testing.T) {
 					q, err := constructor(byteSizeComparator)
 					require.NoError(t, err, "Setup: creating queue with byteSizeComparator should not fail")
 
-					itemLarge := typesmocks.NewMockQueueItemAccessor(100, "itemLarge_prio", flowSpec.ID)
-					itemSmall := typesmocks.NewMockQueueItemAccessor(20, "itemSmall_prio", flowSpec.ID)
-					itemMedium := typesmocks.NewMockQueueItemAccessor(50, "itemMedium_prio", flowSpec.ID)
+					itemLarge := typesmocks.NewMockQueueItemAccessor(100, "itemLarge_prio", flowKey)
+					itemSmall := typesmocks.NewMockQueueItemAccessor(20, "itemSmall_prio", flowKey)
+					itemMedium := typesmocks.NewMockQueueItemAccessor(50, "itemMedium_prio", flowKey)
 
 					itemsInByteSizeOrder := []*typesmocks.MockQueueItemAccessor{itemSmall, itemMedium, itemLarge}
 					testLifecycleAndOrdering(t, q, itemsInByteSizeOrder, "PriorityByteSize")
@@ -238,11 +238,11 @@ func TestQueueConformance(t *testing.T) {
 					require.NoError(t, err, "Setup: creating queue with reverseEnqueueTimeComparator should not fail")
 
 					now := time.Now()
-					item1 := typesmocks.NewMockQueueItemAccessor(100, "item1_lifo", flowSpec.ID)
+					item1 := typesmocks.NewMockQueueItemAccessor(100, "item1_lifo", flowKey)
 					item1.EnqueueTimeV = now.Add(-2 * time.Second) // Earliest
-					item2 := typesmocks.NewMockQueueItemAccessor(50, "item2_lifo", flowSpec.ID)
+					item2 := typesmocks.NewMockQueueItemAccessor(50, "item2_lifo", flowKey)
 					item2.EnqueueTimeV = now.Add(-1 * time.Second) // Middle
-					item3 := typesmocks.NewMockQueueItemAccessor(20, "item3_lifo", flowSpec.ID)
+					item3 := typesmocks.NewMockQueueItemAccessor(20, "item3_lifo", flowKey)
 					item3.EnqueueTimeV = now // Latest
 
 					itemsInLIFOOrder := []*typesmocks.MockQueueItemAccessor{item3, item2, item1}
@@ -268,13 +268,13 @@ func TestQueueConformance(t *testing.T) {
 				q, err := constructor(enqueueTimeComparator)
 				require.NoError(t, err, "Setup: creating queue for test should not fail")
 
-				item := typesmocks.NewMockQueueItemAccessor(100, "item", flowSpec.ID)
+				item := typesmocks.NewMockQueueItemAccessor(100, "item", flowKey)
 				err = q.Add(item)
 				require.NoError(t, err, "Setup: adding an item should succeed")
 
 				otherQ, err := constructor(enqueueTimeComparator) // A different queue instance
 				require.NoError(t, err, "Setup: creating otherQ should succeed")
-				otherItem := typesmocks.NewMockQueueItemAccessor(10, "other_item", "other_flow")
+				otherItem := typesmocks.NewMockQueueItemAccessor(10, "other_item", types.FlowKey{ID: "other-flow"})
 				err = otherQ.Add(otherItem)
 				require.NoError(t, err, "Setup: adding item to otherQ should succeed")
 				alienHandle := otherItem.Handle()
@@ -318,11 +318,11 @@ func TestQueueConformance(t *testing.T) {
 				require.NoError(t, err, "Setup: creating queue for test should not fail")
 
 				now := time.Now()
-				item1 := typesmocks.NewMockQueueItemAccessor(10, "item1_nonhead", flowSpec.ID)
+				item1 := typesmocks.NewMockQueueItemAccessor(10, "item1_nonhead", flowKey)
 				item1.EnqueueTimeV = now.Add(-3 * time.Second)
-				item2 := typesmocks.NewMockQueueItemAccessor(20, "item2_nonhead_TARGET", flowSpec.ID)
+				item2 := typesmocks.NewMockQueueItemAccessor(20, "item2_nonhead_TARGET", flowKey)
 				item2.EnqueueTimeV = now.Add(-2 * time.Second)
-				item3 := typesmocks.NewMockQueueItemAccessor(30, "item3_nonhead", flowSpec.ID)
+				item3 := typesmocks.NewMockQueueItemAccessor(30, "item3_nonhead", flowKey)
 				item3.EnqueueTimeV = now.Add(-1 * time.Second)
 
 				_ = q.Add(item1)
@@ -364,8 +364,8 @@ func TestQueueConformance(t *testing.T) {
 			t.Run("Cleanup_PredicateMatchesNone", func(t *testing.T) {
 				t.Parallel()
 				q, _ := constructor(enqueueTimeComparator)
-				itemK1 := typesmocks.NewMockQueueItemAccessor(10, "k1_matchNone", flowSpec.ID)
-				itemK2 := typesmocks.NewMockQueueItemAccessor(12, "k2_matchNone", flowSpec.ID)
+				itemK1 := typesmocks.NewMockQueueItemAccessor(10, "k1_matchNone", flowKey)
+				itemK2 := typesmocks.NewMockQueueItemAccessor(12, "k2_matchNone", flowKey)
 				_ = q.Add(itemK1)
 				_ = q.Add(itemK2)
 				initialLen := q.Len()
@@ -384,8 +384,8 @@ func TestQueueConformance(t *testing.T) {
 			t.Run("Cleanup_PredicateMatchesAll", func(t *testing.T) {
 				t.Parallel()
 				q, _ := constructor(enqueueTimeComparator)
-				itemR1 := typesmocks.NewMockQueueItemAccessor(11, "r1_matchAll", flowSpec.ID)
-				itemR2 := typesmocks.NewMockQueueItemAccessor(13, "r2_matchAll", flowSpec.ID)
+				itemR1 := typesmocks.NewMockQueueItemAccessor(11, "r1_matchAll", flowKey)
+				itemR2 := typesmocks.NewMockQueueItemAccessor(13, "r2_matchAll", flowKey)
 				_ = q.Add(itemR1)
 				_ = q.Add(itemR2)
 
@@ -401,10 +401,10 @@ func TestQueueConformance(t *testing.T) {
 			t.Run("Cleanup_PredicateMatchesSubset_VerifyHandles", func(t *testing.T) {
 				t.Parallel()
 				q, _ := constructor(enqueueTimeComparator)
-				iK1 := typesmocks.NewMockQueueItemAccessor(20, "k1_subset", flowSpec.ID)
-				iR1 := typesmocks.NewMockQueueItemAccessor(11, "r1_subset", flowSpec.ID)
-				iK2 := typesmocks.NewMockQueueItemAccessor(22, "k2_subset", flowSpec.ID)
-				iR2 := typesmocks.NewMockQueueItemAccessor(33, "r2_subset", flowSpec.ID)
+				iK1 := typesmocks.NewMockQueueItemAccessor(20, "k1_subset", flowKey)
+				iR1 := typesmocks.NewMockQueueItemAccessor(11, "r1_subset", flowKey)
+				iK2 := typesmocks.NewMockQueueItemAccessor(22, "k2_subset", flowKey)
+				iR2 := typesmocks.NewMockQueueItemAccessor(33, "r2_subset", flowKey)
 				_ = q.Add(iK1)
 				_ = q.Add(iR1)
 				_ = q.Add(iK2)
@@ -453,8 +453,8 @@ func TestQueueConformance(t *testing.T) {
 				q, err := constructor(enqueueTimeComparator)
 				require.NoError(t, err, "Setup: creating queue for drain test should not fail")
 
-				itemD1 := typesmocks.NewMockQueueItemAccessor(10, "ditem1", flowSpec.ID)
-				itemD2 := typesmocks.NewMockQueueItemAccessor(20, "ditem2", flowSpec.ID)
+				itemD1 := typesmocks.NewMockQueueItemAccessor(10, "ditem1", flowKey)
+				itemD2 := typesmocks.NewMockQueueItemAccessor(20, "ditem2", flowKey)
 				_ = q.Add(itemD1)
 				_ = q.Add(itemD2)
 
@@ -512,7 +512,7 @@ func TestQueueConformance(t *testing.T) {
 
 				// Pre-populate the queue with an initial set of items.
 				for i := 0; i < initialItems; i++ {
-					item := typesmocks.NewMockQueueItemAccessor(1, fmt.Sprintf("%s_conc_init_%d", flowSpec.ID, i), flowSpec.ID)
+					item := typesmocks.NewMockQueueItemAccessor(1, fmt.Sprintf("%s_conc_init_%d", flowKey, i), flowKey)
 					err := q.Add(item)
 					require.NoError(t, err, "Setup: pre-populating the queue should not fail")
 					handleChan <- item.Handle()
@@ -531,7 +531,7 @@ func TestQueueConformance(t *testing.T) {
 							switch opType {
 							case 0: // Add
 								item := typesmocks.NewMockQueueItemAccessor(1,
-									fmt.Sprintf("%s_conc_init_%d_%d", flowSpec.ID, routineID, j), flowSpec.ID)
+									fmt.Sprintf("%s_conc_init_%d_%d", flowKey, routineID, j), flowKey)
 								err := q.Add(item)
 								if assert.NoError(t, err, "Add must be goroutine-safe") {
 									successfulAdds.Add(1)
diff --git a/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go b/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go
index 39e82fb..22a6290 100644
--- a/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go
+++ b/pkg/epp/flowcontrol/framework/plugins/queue/maxminheap/maxminheap_test.go
@@ -46,7 +46,7 @@ func TestMaxMinHeap_InternalProperty(t *testing.T) {
 	now := time.Now()
 	for i := range items {
 		// Add items in a somewhat random order of enqueue times
-		items[i] = typesmocks.NewMockQueueItemAccessor(10, "item", "flow")
+		items[i] = typesmocks.NewMockQueueItemAccessor(10, "item", types.FlowKey{ID: "flow"})
 		items[i].EnqueueTimeV = now.Add(time.Duration((i%5-2)*10) * time.Second)
 		err := q.Add(items[i])
 		require.NoError(t, err, "Add should not fail")
diff --git a/pkg/epp/flowcontrol/registry/managedqueue_test.go b/pkg/epp/flowcontrol/registry/managedqueue_test.go
index 6fc172f..ff73457 100644
--- a/pkg/epp/flowcontrol/registry/managedqueue_test.go
+++ b/pkg/epp/flowcontrol/registry/managedqueue_test.go
@@ -26,7 +26,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/contracts"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework"
 	frameworkmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/mocks"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/framework/plugins/queue"
@@ -35,6 +34,8 @@ import (
 	typesmocks "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/flowcontrol/types/mocks"
 )
 
+var testFlowKey = types.FlowKey{ID: "test-flow", Priority: 1}
+
 // testStatsReconciler is a mock implementation of the `parentStatsReconciler` function.
 // It captures the deltas it's called with, allowing tests to assert on them.
 type testStatsReconciler struct {
@@ -64,7 +65,7 @@ type testFixture struct {
 	mockQueue      *frameworkmocks.MockSafeQueue
 	mockPolicy     *frameworkmocks.MockIntraFlowDispatchPolicy
 	reconciler     *testStatsReconciler
-	flowSpec       types.FlowSpecification
+	flowKey        types.FlowKey
 	mockComparator *frameworkmocks.MockItemComparator
 }
 
@@ -74,7 +75,7 @@ func setupTestManagedQueue(t *testing.T) *testFixture {
 
 	mockQueue := &frameworkmocks.MockSafeQueue{}
 	reconciler := &testStatsReconciler{}
-	flowSpec := types.FlowSpecification{ID: "test-flow", Priority: 1}
+	flowKey := types.FlowKey{ID: "test-flow", Priority: 1}
 	mockComparator := &frameworkmocks.MockItemComparator{}
 	mockPolicy := &frameworkmocks.MockIntraFlowDispatchPolicy{
 		ComparatorV: mockComparator,
@@ -83,7 +84,7 @@ func setupTestManagedQueue(t *testing.T) *testFixture {
 	mq := newManagedQueue(
 		mockQueue,
 		mockPolicy,
-		flowSpec,
+		flowKey,
 		logr.Discard(),
 		reconciler.reconcile,
 	)
@@ -94,7 +95,7 @@ func setupTestManagedQueue(t *testing.T) *testFixture {
 		mockQueue:      mockQueue,
 		mockPolicy:     mockPolicy,
 		reconciler:     reconciler,
-		flowSpec:       flowSpec,
+		flowKey:        flowKey,
 		mockComparator: mockComparator,
 	}
 }
@@ -105,7 +106,6 @@ func TestManagedQueue_New(t *testing.T) {
 
 	assert.Zero(t, f.mq.Len(), "A new managedQueue should have a length of 0")
 	assert.Zero(t, f.mq.ByteSize(), "A new managedQueue should have a byte size of 0")
-	assert.True(t, f.mq.isActive.Load(), "A new managedQueue should be active")
 }
 
 func TestManagedQueue_Add(t *testing.T) {
@@ -115,9 +115,7 @@ func TestManagedQueue_Add(t *testing.T) {
 		name                  string
 		itemByteSize          uint64
 		mockAddError          error
-		markAsDraining        bool
 		expectError           bool
-		expectedErrorIs       error
 		expectedLen           int
 		expectedByteSize      uint64
 		expectedLenDelta      int64
@@ -145,18 +143,6 @@ func TestManagedQueue_Add(t *testing.T) {
 			expectedByteSizeDelta: 0,
 			expectedReconcile:     false,
 		},
-		{
-			name:                  "Error on inactive queue",
-			itemByteSize:          100,
-			markAsDraining:        true,
-			expectError:           true,
-			expectedErrorIs:       contracts.ErrFlowInstanceNotFound,
-			expectedLen:           0,
-			expectedByteSize:      0,
-			expectedLenDelta:      0,
-			expectedByteSizeDelta: 0,
-			expectedReconcile:     false,
-		},
 	}
 
 	for _, tc := range testCases {
@@ -169,19 +155,11 @@ func TestManagedQueue_Add(t *testing.T) {
 				return tc.mockAddError
 			}
 
-			if tc.markAsDraining {
-				f.mq.markAsDraining()
-				assert.False(t, f.mq.isActive.Load(), "Setup: queue should be marked as inactive")
-			}
-
-			item := typesmocks.NewMockQueueItemAccessor(tc.itemByteSize, "req-1", "test-flow")
+			item := typesmocks.NewMockQueueItemAccessor(tc.itemByteSize, "req-1", testFlowKey)
 			err := f.mq.Add(item)
 
 			if tc.expectError {
 				require.Error(t, err, "Add should have returned an error")
-				if tc.expectedErrorIs != nil {
-					assert.ErrorIs(t, err, tc.expectedErrorIs, "Error should wrap the expected sentinel error")
-				}
 			} else {
 				require.NoError(t, err, "Add should not have returned an error")
 			}
@@ -208,7 +186,7 @@ func TestManagedQueue_Remove(t *testing.T) {
 	f := setupTestManagedQueue(t)
 
 	// Setup initial state
-	initialItem := typesmocks.NewMockQueueItemAccessor(100, "req-1", "test-flow")
+	initialItem := typesmocks.NewMockQueueItemAccessor(100, "req-1", testFlowKey)
 	f.mockQueue.AddFunc = func(item types.QueueItemAccessor) error { return nil }
 	err := f.mq.Add(initialItem)
 	require.NoError(t, err, "Setup: Adding an item should not fail")
@@ -263,9 +241,9 @@ func TestManagedQueue_Remove(t *testing.T) {
 
 func TestManagedQueue_CleanupAndDrain(t *testing.T) {
 	t.Parallel()
-	item1 := typesmocks.NewMockQueueItemAccessor(10, "req-1", "test-flow")
-	item2 := typesmocks.NewMockQueueItemAccessor(20, "req-2", "test-flow")
-	item3 := typesmocks.NewMockQueueItemAccessor(30, "req-3", "test-flow")
+	item1 := typesmocks.NewMockQueueItemAccessor(10, "req-1", testFlowKey)
+	item2 := typesmocks.NewMockQueueItemAccessor(20, "req-2", testFlowKey)
+	item3 := typesmocks.NewMockQueueItemAccessor(30, "req-3", testFlowKey)
 
 	// --- Test Cleanup ---
 	t.Run("Cleanup", func(t *testing.T) {
@@ -359,7 +337,7 @@ func TestManagedQueue_CleanupAndDrain(t *testing.T) {
 func TestManagedQueue_FlowQueueAccessor(t *testing.T) {
 	t.Parallel()
 	f := setupTestManagedQueue(t)
-	item := typesmocks.NewMockQueueItemAccessor(100, "req-1", "test-flow")
+	item := typesmocks.NewMockQueueItemAccessor(100, "req-1", testFlowKey)
 
 	// Setup underlying queue state
 	f.mockQueue.PeekHeadV = item
@@ -379,7 +357,7 @@ func TestManagedQueue_FlowQueueAccessor(t *testing.T) {
 	assert.Equal(t, f.mq.Capabilities(), accessor.Capabilities(), "Accessor Capabilities() should match managed queue")
 	assert.Equal(t, f.mq.Len(), accessor.Len(), "Accessor Len() should match managed queue")
 	assert.Equal(t, f.mq.ByteSize(), accessor.ByteSize(), "Accessor ByteSize() should match managed queue")
-	assert.Equal(t, f.flowSpec, accessor.FlowSpec(), "Accessor FlowSpec() should match managed queue")
+	assert.Equal(t, f.flowKey, accessor.FlowKey(), "Accessor FlowKey() should match managed queue")
 	assert.Equal(t, f.mockComparator, accessor.Comparator(), "Accessor Comparator() should match the one from the policy")
 	assert.Equal(t, f.mockComparator, f.mq.Comparator(), "ManagedQueue Comparator() should match the one from the policy")
 
@@ -400,8 +378,8 @@ func TestManagedQueue_Concurrency(t *testing.T) {
 	require.NoError(t, err, "Setup: creating a real listqueue should not fail")
 
 	reconciler := &testStatsReconciler{}
-	flowSpec := types.FlowSpecification{ID: "conc-test-flow", Priority: 1}
-	mq := newManagedQueue(lq, nil, flowSpec, logr.Discard(), reconciler.reconcile)
+	flowKey := types.FlowKey{ID: "conc-test-flow", Priority: 1}
+	mq := newManagedQueue(lq, nil, flowKey, logr.Discard(), reconciler.reconcile)
 
 	const (
 		numGoroutines   = 20
@@ -418,7 +396,7 @@ func TestManagedQueue_Concurrency(t *testing.T) {
 
 	// Pre-fill the queue to give removers something to do immediately.
 	for range initialItems {
-		item := typesmocks.NewMockQueueItemAccessor(uint64(itemByteSize), "initial", "flow")
+		item := typesmocks.NewMockQueueItemAccessor(uint64(itemByteSize), "initial", testFlowKey)
 		require.NoError(t, mq.Add(item), "Setup: pre-filling queue should not fail")
 		handles <- item.Handle()
 	}
@@ -434,7 +412,7 @@ func TestManagedQueue_Concurrency(t *testing.T) {
 				// Mix up operations between adding and removing.
 				if (routineID+j)%2 == 0 {
 					// Add operation
-					item := typesmocks.NewMockQueueItemAccessor(uint64(itemByteSize), "req", "flow")
+					item := typesmocks.NewMockQueueItemAccessor(uint64(itemByteSize), "req", testFlowKey)
 					if err := mq.Add(item); err == nil {
 						successfulAdds.Add(1)
 						handles <- item.Handle()
diff --git a/pkg/epp/flowcontrol/registry/shard_test.go b/pkg/epp/flowcontrol/registry/shard_test.go
index fe7f96e..b04bde4 100644
--- a/pkg/epp/flowcontrol/registry/shard_test.go
+++ b/pkg/epp/flowcontrol/registry/shard_test.go
@@ -18,7 +18,7 @@ package registry
 
 import (
 	"errors"
-	"sort"
+	"slices"
 	"testing"
 
 	"github.com/go-logr/logr"
@@ -75,17 +75,16 @@ func setupTestShard(t *testing.T) *shardTestFixture {
 }
 
 // _reconcileFlow_testOnly is a test helper that simulates the future admin logic for adding or updating a flow.
-// It creates a `managedQueue` and correctly populates the `priorityBands` and `activeFlows` maps.
+// It creates a `managedQueue` and correctly populates the `priorityBands` map and the `flowKeys` slice.
 // This helper is intended to be replaced by the real `reconcileFlow` method in a future PR.
 func (s *registryShard) _reconcileFlow_testOnly(
 	t *testing.T,
 	flowSpec types.FlowSpecification,
-	isActive bool,
 ) *managedQueue {
 	t.Helper()
 
-	band, ok := s.priorityBands[flowSpec.Priority]
-	require.True(t, ok, "Setup: priority band %d should exist", flowSpec.Priority)
+	band, ok := s.priorityBands[flowSpec.Key.Priority]
+	require.True(t, ok, "Setup: priority band %d should exist", flowSpec.Key.Priority)
 
 	lq, err := queue.NewQueueFromName(listqueue.ListQueueName, nil)
 	require.NoError(t, err, "Setup: creating a real listqueue should not fail")
@@ -93,16 +92,15 @@ func (s *registryShard) _reconcileFlow_testOnly(
 	mq := newManagedQueue(
 		lq,
 		band.defaultIntraFlowDispatchPolicy,
-		flowSpec,
+		flowSpec.Key,
 		logr.Discard(),
-		func(lenDelta, byteSizeDelta int64) { s.reconcileStats(flowSpec.Priority, lenDelta, byteSizeDelta) },
+		func(lenDelta, byteSizeDelta int64) { s.reconcileStats(flowSpec.Key.Priority, lenDelta, byteSizeDelta) },
 	)
 	require.NotNil(t, mq, "Setup: newManagedQueue should not return nil")
 
-	band.queues[flowSpec.ID] = mq
-	if isActive {
-		s.activeFlows[flowSpec.ID] = mq
-	}
+	// Add the queue to the map and update the cached key slice.
+	band.queues[flowSpec.Key] = mq
+	band.flowKeys = append(band.flowKeys, flowSpec.Key)
 
 	return mq
 }
@@ -188,11 +186,12 @@ func TestShard_Stats(t *testing.T) {
 	f := setupTestShard(t)
 
 	// Add a queue and some items to test stats aggregation
-	mq := f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{ID: "flow1", Priority: 10}, true)
+	flowKey := types.FlowKey{ID: "flow1", Priority: 10}
+	mq := f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{Key: flowKey})
 
 	// Add items
-	require.NoError(t, mq.Add(mocks.NewMockQueueItemAccessor(100, "req1", "flow1")), "Adding item should not fail")
-	require.NoError(t, mq.Add(mocks.NewMockQueueItemAccessor(50, "req2", "flow1")), "Adding item should not fail")
+	require.NoError(t, mq.Add(mocks.NewMockQueueItemAccessor(100, "req1", flowKey)), "Adding item should not fail")
+	require.NoError(t, mq.Add(mocks.NewMockQueueItemAccessor(50, "req2", flowKey)), "Adding item should not fail")
 
 	stats := f.shard.Stats()
 
@@ -217,67 +216,44 @@ func TestShard_Accessors(t *testing.T) {
 	t.Parallel()
 	f := setupTestShard(t)
 
-	flowID := "test-flow"
-	activePriority := uint(10)
-	drainingPriority := uint(20)
-
-	// Setup state with one active and one draining queue for the same flow
-	activeQueue := f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{
-		ID:       flowID,
-		Priority: activePriority,
-	}, true)
-	drainingQueue := f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{
-		ID:       flowID,
-		Priority: drainingPriority,
-	}, false)
-
-	t.Run("ActiveManagedQueue", func(t *testing.T) {
-		t.Parallel()
-		retrievedActiveQueue, err := f.shard.ActiveManagedQueue(flowID)
-		require.NoError(t, err, "ActiveManagedQueue should not error for an existing flow")
-		assert.Same(t, activeQueue, retrievedActiveQueue, "Should return the correct active queue")
+	key1 := types.FlowKey{ID: "flow1", Priority: 10}
+	key2 := types.FlowKey{ID: "flow2", Priority: 20}
 
-		_, err = f.shard.ActiveManagedQueue("non-existent-flow")
-		require.Error(t, err, "ActiveManagedQueue should error for a non-existent flow")
-		assert.ErrorIs(t, err, contracts.ErrFlowInstanceNotFound, "Error should be ErrFlowInstanceNotFound")
-	})
+	// Setup state
+	queue1 := f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{Key: key1})
+	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{Key: key2})
 
 	t.Run("ManagedQueue", func(t *testing.T) {
 		t.Parallel()
-		retrievedDrainingQueue, err := f.shard.ManagedQueue(flowID, drainingPriority)
-		require.NoError(t, err, "ManagedQueue should not error for a draining queue")
-		assert.Same(t, drainingQueue, retrievedDrainingQueue, "Should return the correct draining queue")
+		retrievedQueue, err := f.shard.ManagedQueue(key1)
+		require.NoError(t, err, "ManagedQueue should not error for an existing key")
+		assert.Same(t, queue1, retrievedQueue, "Should return the correct queue")
 
-		_, err = f.shard.ManagedQueue(flowID, 99) // Non-existent priority
+		_, err = f.shard.ManagedQueue(types.FlowKey{ID: "non-existent", Priority: 10})
+		require.Error(t, err, "ManagedQueue should error for a non-existent flow key")
+		assert.ErrorIs(t, err, contracts.ErrFlowInstanceNotFound, "Error should be ErrFlowInstanceNotFound")
+
+		_, err = f.shard.ManagedQueue(types.FlowKey{ID: "flow1", Priority: 99}) // Non-existent priority
 		require.Error(t, err, "ManagedQueue should error for a non-existent priority")
 		assert.ErrorIs(t, err, contracts.ErrPriorityBandNotFound, "Error should be ErrPriorityBandNotFound")
-
-		_, err = f.shard.ManagedQueue("non-existent-flow", activePriority)
-		require.Error(t, err, "ManagedQueue should error for a non-existent flow in an existing priority")
-		assert.ErrorIs(t, err, contracts.ErrFlowInstanceNotFound, "Error should be ErrFlowInstanceNotFound")
 	})
 
 	t.Run("IntraFlowDispatchPolicy", func(t *testing.T) {
 		t.Parallel()
-		retrievedActivePolicy, err := f.shard.IntraFlowDispatchPolicy(flowID, activePriority)
-		require.NoError(t, err, "IntraFlowDispatchPolicy should not error for an active instance")
-		assert.Same(t, activeQueue.dispatchPolicy, retrievedActivePolicy,
-			"Should return the policy from the active instance")
+		retrievedPolicy, err := f.shard.IntraFlowDispatchPolicy(key1)
+		require.NoError(t, err, "IntraFlowDispatchPolicy should not error for an existing key")
+		assert.Same(t, queue1.dispatchPolicy, retrievedPolicy, "Should return the policy from the correct instance")
 
-		_, err = f.shard.IntraFlowDispatchPolicy("non-existent-flow", activePriority)
+		_, err = f.shard.IntraFlowDispatchPolicy(types.FlowKey{ID: "non-existent", Priority: 10})
 		require.Error(t, err, "IntraFlowDispatchPolicy should error for a non-existent flow")
 		assert.ErrorIs(t, err, contracts.ErrFlowInstanceNotFound, "Error should be ErrFlowInstanceNotFound")
-
-		_, err = f.shard.IntraFlowDispatchPolicy(flowID, 99) // Non-existent priority
-		require.Error(t, err, "IntraFlowDispatchPolicy should error for a non-existent priority")
-		assert.ErrorIs(t, err, contracts.ErrPriorityBandNotFound, "Error should be ErrPriorityBandNotFound")
 	})
 
 	t.Run("InterFlowDispatchPolicy", func(t *testing.T) {
 		t.Parallel()
-		retrievedInterPolicy, err := f.shard.InterFlowDispatchPolicy(activePriority)
+		retrievedInterPolicy, err := f.shard.InterFlowDispatchPolicy(10)
 		require.NoError(t, err, "InterFlowDispatchPolicy should not error for an existing priority")
-		assert.Same(t, f.shard.priorityBands[activePriority].interFlowDispatchPolicy, retrievedInterPolicy,
+		assert.Same(t, f.shard.priorityBands[10].interFlowDispatchPolicy, retrievedInterPolicy,
 			"Should return the correct inter-flow policy")
 
 		_, err = f.shard.InterFlowDispatchPolicy(99) // Non-existent priority
@@ -292,57 +268,66 @@ func TestShard_PriorityBandAccessor(t *testing.T) {
 
 	// Setup shard state for the tests
 	p1, p2 := uint(10), uint(20)
-	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{ID: "flow1", Priority: p1}, true)
-	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{ID: "flow1", Priority: p2}, false)
-	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{ID: "flow2", Priority: p1}, true)
+	key1P1 := types.FlowKey{ID: "flow1", Priority: p1}
+	key1P2 := types.FlowKey{ID: "flow1", Priority: p2}
+	key2P2 := types.FlowKey{ID: "flow2", Priority: p2}
+
+	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{Key: key1P1})
+	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{Key: key1P2})
+	f.shard._reconcileFlow_testOnly(t, types.FlowSpecification{Key: key2P2})
 
 	t.Run("Accessor for existing priority", func(t *testing.T) {
 		t.Parallel()
-		accessor, err := f.shard.PriorityBandAccessor(p1)
+		accessor, err := f.shard.PriorityBandAccessor(p2)
 		require.NoError(t, err, "PriorityBandAccessor should not fail for existing priority")
 		require.NotNil(t, accessor, "Accessor should not be nil")
 
 		t.Run("Properties", func(t *testing.T) {
 			t.Parallel()
-			assert.Equal(t, p1, accessor.Priority(), "Accessor should have correct priority")
-			assert.Equal(t, "High", accessor.PriorityName(), "Accessor should have correct priority name")
+			assert.Equal(t, p2, accessor.Priority(), "Accessor should have correct priority")
+			assert.Equal(t, "Low", accessor.PriorityName(), "Accessor should have correct priority name")
 		})
 
-		t.Run("FlowIDs", func(t *testing.T) {
+		t.Run("FlowKeys", func(t *testing.T) {
 			t.Parallel()
-			flowIDs := accessor.FlowIDs()
-			sort.Strings(flowIDs)
-			assert.Equal(t, []string{"flow1", "flow2"}, flowIDs,
-				"Accessor should return correct flow IDs for the priority band")
+			flowKeys := accessor.FlowKeys()
+			// Sort for consistent comparison as map iteration order is not guaranteed.
+			slices.SortFunc(flowKeys, func(a, b types.FlowKey) int {
+				return a.Compare(b)
+			})
+			assert.Equal(t, []types.FlowKey{key1P2, key2P2}, flowKeys,
+				"Accessor should return correct flow keys for the priority band")
 		})
 
 		t.Run("Queue", func(t *testing.T) {
 			t.Parallel()
 			q := accessor.Queue("flow1")
 			require.NotNil(t, q, "Accessor should return queue for flow1")
-			assert.Equal(t, p1, q.FlowSpec().Priority, "Queue should have the correct priority")
+			assert.Equal(t, p2, q.FlowKey().Priority, "Queue should have the correct priority")
 			assert.Nil(t, accessor.Queue("non-existent"), "Accessor should return nil for non-existent flow")
 		})
 
 		t.Run("IterateQueues", func(t *testing.T) {
 			t.Parallel()
-			var iteratedFlows []string
+			var iteratedKeys []types.FlowKey
 			accessor.IterateQueues(func(queue framework.FlowQueueAccessor) bool {
-				iteratedFlows = append(iteratedFlows, queue.FlowSpec().ID)
+				iteratedKeys = append(iteratedKeys, queue.FlowKey())
 				return true
 			})
-			sort.Strings(iteratedFlows)
-			assert.Equal(t, []string{"flow1", "flow2"}, iteratedFlows, "IterateQueues should visit all flows in the band")
+			slices.SortFunc(iteratedKeys, func(a, b types.FlowKey) int {
+				return a.Compare(b)
+			})
+			assert.Equal(t, []types.FlowKey{key1P2, key2P2}, iteratedKeys, "IterateQueues should visit all flows in the band")
 		})
 
 		t.Run("IterateQueues with early exit", func(t *testing.T) {
 			t.Parallel()
-			var iteratedFlows []string
+			var iteratedKeys []types.FlowKey
 			accessor.IterateQueues(func(queue framework.FlowQueueAccessor) bool {
-				iteratedFlows = append(iteratedFlows, queue.FlowSpec().ID)
+				iteratedKeys = append(iteratedKeys, queue.FlowKey())
 				return false // Exit after first item
 			})
-			assert.Len(t, iteratedFlows, 1, "IterateQueues should exit early if callback returns false")
+			assert.Len(t, iteratedKeys, 1, "IterateQueues should exit early if callback returns false")
 		})
 	})
 
diff --git a/pkg/epp/metrics/collectors/inference_pool_test.go b/pkg/epp/metrics/collectors/inference_pool_test.go
index 533d7f3..dcac3b3 100644
--- a/pkg/epp/metrics/collectors/inference_pool_test.go
+++ b/pkg/epp/metrics/collectors/inference_pool_test.go
@@ -49,7 +49,7 @@ var (
 )
 
 func TestNoMetricsCollected(t *testing.T) {
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 	datastore := datastore.NewDatastore(context.Background(), pmf)
 
 	collector := &inferencePoolMetricsCollector{
@@ -67,7 +67,7 @@ func TestMetricsCollected(t *testing.T) {
 			pod1NamespacedName: pod1Metrics,
 		},
 	}
-	pmf := backendmetrics.NewPodMetricsFactory(pmc, time.Millisecond, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(pmc, time.Millisecond)
 	ds := datastore.NewDatastore(context.Background(), pmf)
 
 	scheme := runtime.NewScheme()
@@ -80,7 +80,7 @@ func TestMetricsCollected(t *testing.T) {
 			Name: "test-pool",
 		},
 		Spec: v1.InferencePoolSpec{
-			TargetPortNumber: 8000,
+			TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
 		},
 	}
 	_ = ds.PoolSet(context.Background(), fakeClient, inferencePool)
diff --git a/pkg/epp/plugins/plugin_state_test.go b/pkg/epp/plugins/plugin_state_test.go
new file mode 100644
index 0000000..46c0de8
--- /dev/null
+++ b/pkg/epp/plugins/plugin_state_test.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package plugins
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+// pluginTestData implements the StateData interface for testing purposes.
+// It provides a simple string value that can be stored and retrieved.
+type pluginTestData struct {
+	value string
+}
+
+// Clone implements the StateData interface, creating a deep copy of the data.
+func (d *pluginTestData) Clone() StateData {
+	if d == nil {
+		return nil
+	}
+	return &pluginTestData{value: d.value}
+}
+
+// TestPluginState_ReadWrite verifies the basic operations of PluginState:
+// - Writing data for a request
+// - Reading the data back
+// - Deleting the data and confirming it's removed
+func TestPluginState_ReadWrite(t *testing.T) {
+	ctx := logutil.NewTestLoggerIntoContext(context.Background())
+
+	state := NewPluginState(ctx)
+
+	req1 := "req1"
+	key := StateKey("key")
+	data1 := "bar1"
+	req2 := "req2"
+	data2 := "bar2"
+
+	// Write data to the state storage
+	state.Write(req1, key, &pluginTestData{value: data1})
+	state.Write(req2, key, &pluginTestData{value: data2})
+
+	// Read back the req1 data and verify its content
+	readData, err := state.Read(req1, key)
+	assert.NoError(t, err)
+	td, ok := readData.(*pluginTestData)
+	assert.True(t, ok, "should be able to cast to pluginTestData")
+	assert.Equal(t, data1, td.value)
+
+	// Delete the req2 data and verify it's removed
+	state.Delete(req2)
+	readData, err = state.Read(req2, key)
+	assert.Equal(t, ErrNotFound, err)
+	assert.Nil(t, readData, "expected no data after delete")
+
+	// Read back the req1 data and verify its content after the req2 deleted
+	readData, err = state.Read(req1, key)
+	assert.NoError(t, err)
+	td, ok = readData.(*pluginTestData)
+	assert.True(t, ok, "should be able to cast to pluginTestData")
+	assert.Equal(t, data1, td.value)
+}
+
+// TestReadPluginStateKey tests the generic helper function ReadPluginStateKey which provides
+// type-safe access to stored data. It verifies:
+// - Successful type assertion and data retrieval
+// - Error handling for non-existent keys
+func TestReadPluginStateKey(t *testing.T) {
+	ctx := logutil.NewTestLoggerIntoContext(context.Background())
+	state := NewPluginState(ctx)
+
+	requestID := "req-1"
+	key := StateKey("foo")
+	data := &pluginTestData{value: "bar"}
+
+	state.Write(requestID, key, data)
+
+	// Read
+	val, err := ReadPluginStateKey[*pluginTestData](state, requestID, key)
+	assert.NoError(t, err)
+	assert.Equal(t, "bar", val.value)
+
+	// Not Found
+	_, err = ReadPluginStateKey[*pluginTestData](state, "not-exist", key)
+	assert.Equal(t, ErrNotFound, err)
+}
+
+// TestPluginState_Cleanup verifies the automatic cleanup of stale data.
+// It tests that data which hasn't been accessed for longer than stalenessThreshold
+// is properly removed from the storage.
+func TestPluginState_Cleanup(t *testing.T) {
+	ctx := logutil.NewTestLoggerIntoContext(context.Background())
+
+	state := NewPluginState(ctx)
+
+	requestID := "req-stale"
+	key := StateKey("foo")
+	data := &pluginTestData{value: "bar"}
+
+	state.Write(requestID, key, data)
+
+	// Manually set last access time to far in the past
+	state.requestToLastAccessTime.Store(requestID, time.Now().Add(-2*stalenessThreshold))
+	// Manually CleanUp
+	state.cleanStaleRequests()
+
+	_, err := state.Read(requestID, key)
+	assert.Equal(t, ErrNotFound, err)
+}
diff --git a/pkg/epp/requestcontrol/director_test.go b/pkg/epp/requestcontrol/director_test.go
index 3a98b28..e7968d4 100644
--- a/pkg/epp/requestcontrol/director_test.go
+++ b/pkg/epp/requestcontrol/director_test.go
@@ -34,7 +34,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	v1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
-	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
@@ -81,20 +80,19 @@ func TestDirector_HandleRequest(t *testing.T) {
 	// InferenceObjective definitions
 	ioFoodReview := testutil.MakeInferenceObjective("ioFoodReview").
 		CreationTimestamp(metav1.Unix(1000, 0)).
-		Criticality(v1alpha2.Critical).
+		Priority(2).
 		ObjRef()
 	ioFoodReviewSheddable := testutil.MakeInferenceObjective("imFoodReviewSheddable").
 		CreationTimestamp(metav1.Unix(1000, 0)).
-		Criticality(v1alpha2.Sheddable).
+		Priority(-1).
 		ObjRef()
 	ioFoodReviewResolve := testutil.MakeInferenceObjective("imFoodReviewResolve").
 		CreationTimestamp(metav1.Unix(1000, 0)).
-		Criticality(v1alpha2.Standard).
-		TargetModel("resolved-target-model-A").
+		Priority(1).
 		ObjRef()
 
 	// Datastore setup
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 	ds := datastore.NewDatastore(t.Context(), pmf)
 	ds.ObjectiveSet(ioFoodReview)
 	ds.ObjectiveSet(ioFoodReviewResolve)
@@ -103,9 +101,11 @@ func TestDirector_HandleRequest(t *testing.T) {
 	pool := &v1.InferencePool{
 		ObjectMeta: metav1.ObjectMeta{Name: "test-pool", Namespace: "default"},
 		Spec: v1.InferencePoolSpec{
-			TargetPortNumber: int32(8000),
-			Selector: map[v1.LabelKey]v1.LabelValue{
-				"app": "inference",
+			TargetPorts: []v1.Port{{Number: v1.PortNumber(int32(8000))}},
+			Selector: v1.LabelSelector{
+				MatchLabels: map[v1.LabelKey]v1.LabelValue{
+					"app": "inference",
+				},
 			},
 		},
 	}
@@ -177,6 +177,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 		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)",
@@ -199,9 +200,10 @@ func TestDirector_HandleRequest(t *testing.T) {
 			},
 			wantMutatedBodyModel:   model,
 			inferenceObjectiveName: objectiveName,
+			targetModelName:        model,
 		},
 		{
-			name: "successful chat completions request (critical, saturation ignored)",
+			name: "successful chat completions request (default critical, saturation ignored)",
 			reqBodyMap: map[string]any{
 				"model": model,
 				"messages": []any{
@@ -211,11 +213,11 @@ func TestDirector_HandleRequest(t *testing.T) {
 					},
 				},
 			},
+			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				ObjectiveKey:    objectiveName,
 				TargetModelName: model,
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
@@ -223,8 +225,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 				},
 				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
 			},
-			wantMutatedBodyModel:   model,
-			inferenceObjectiveName: objectiveName,
+			wantMutatedBodyModel: model,
+			targetModelName:      model,
 		},
 		{
 			name: "successful chat completions request with multiple messages (critical, saturation ignored)",
@@ -255,6 +257,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			},
 			wantMutatedBodyModel:   model,
 			inferenceObjectiveName: objectiveName,
+			targetModelName:        model,
 		},
 		{
 			name: "successful completions request (sheddable, not saturated)",
@@ -277,6 +280,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			},
 			wantMutatedBodyModel:   modelSheddable,
 			inferenceObjectiveName: objectiveNameSheddable,
+			targetModelName:        modelSheddable,
 		},
 		{
 			name: "successful request with target model resolution",
@@ -299,6 +303,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			},
 			wantMutatedBodyModel:   "resolved-target-model-A",
 			inferenceObjectiveName: objectiveNameResolve,
+			targetModelName:        "resolved-target-model-A",
 		},
 		{
 			name: "nonexistent target defined, use default inference model",
@@ -321,6 +326,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 			},
 			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
 			inferenceObjectiveName: "food-review-1",
+			targetModelName:        "food-review-1",
 		},
 		{
 
@@ -329,6 +335,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 				"model":  modelSheddable,
 				"prompt": "sheddable prompt",
 			},
+			inferenceObjectiveName: objectiveNameSheddable,
 			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
 			wantErrCode:            errutil.InferencePoolResourceExhausted,
 		},
@@ -395,7 +402,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 						requtil.RequestIdHeaderKey: "test-req-id-" + test.name, // Ensure a default request ID
 					},
 				},
-				ObjectiveKey: test.inferenceObjectiveName,
+				ObjectiveKey:    test.inferenceObjectiveName,
+				TargetModelName: test.targetModelName,
 			}
 			// Deep copy the body map.
 			for k, v := range test.reqBodyMap {
@@ -542,7 +550,7 @@ func TestGetCandidatePodsForScheduling(t *testing.T) {
 		},
 	}
 
-	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Second)
 	ds := datastore.NewDatastore(t.Context(), pmf)
 	for _, testPod := range testInput {
 		ds.PodUpdateOrAddIfNotExist(testPod)
@@ -564,76 +572,6 @@ func TestGetCandidatePodsForScheduling(t *testing.T) {
 	}
 }
 
-func TestRandomWeightedDraw(t *testing.T) {
-	logger := logutil.NewTestLogger()
-	// Note: These tests verify deterministic outcomes for a fixed seed (420).
-	// They do not test the statistical properties of the random draw.
-	tests := []struct {
-		name  string
-		model *v1alpha2.InferenceObjective
-		want  string
-	}{
-		{
-			name: "deterministic draw: 50/50 weights, seed 420",
-			model: &v1alpha2.InferenceObjective{
-				Spec: v1alpha2.InferenceObjectiveSpec{
-					TargetModels: []v1alpha2.TargetModel{
-						{Name: "canary", Weight: pointer(50)},
-						{Name: "v1", Weight: pointer(50)},
-					},
-				},
-			},
-			want: "canary",
-		},
-		{
-			name: "deterministic draw: 25/55/50 weights, seed 420",
-			model: &v1alpha2.InferenceObjective{
-				Spec: v1alpha2.InferenceObjectiveSpec{
-					TargetModels: []v1alpha2.TargetModel{
-						{Name: "canary", Weight: pointer(25)},
-						{Name: "v1.1", Weight: pointer(55)},
-						{Name: "v1", Weight: pointer(50)},
-					},
-				},
-			},
-			want: "v1",
-		},
-		{
-			name: "deterministic draw: 20/20/10 weights, seed 420",
-			model: &v1alpha2.InferenceObjective{
-				Spec: v1alpha2.InferenceObjectiveSpec{
-					TargetModels: []v1alpha2.TargetModel{
-						{Name: "canary", Weight: pointer(20)},
-						{Name: "v1.1", Weight: pointer(20)},
-						{Name: "v1", Weight: pointer(10)},
-					},
-				},
-			},
-			want: "v1.1",
-		},
-		{
-			name: "deterministic draw: nil weights (uniform), seed 420",
-			model: &v1alpha2.InferenceObjective{
-				Spec: v1alpha2.InferenceObjectiveSpec{
-					TargetModels: []v1alpha2.TargetModel{
-						{Name: "canary"},
-						{Name: "v1.1"},
-						{Name: "v1"},
-					},
-				},
-			},
-			want: "canary",
-		},
-	}
-	var seedVal int64 = 420
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			model := RandomWeightedDraw(logger, test.model, seedVal)
-			assert.Equal(t, test.want, model, "RandomWeightedDraw() with seed %d should produce expected model", seedVal)
-		})
-	}
-}
-
 func TestGetRandomPod(t *testing.T) {
 	tests := []struct {
 		name      string
@@ -665,7 +603,7 @@ func TestGetRandomPod(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Millisecond, time.Second*2)
+			pmf := backendmetrics.NewPodMetricsFactory(&backendmetrics.FakePodMetricsClient{}, time.Millisecond)
 			ds := datastore.NewDatastore(t.Context(), pmf)
 			for _, pod := range test.storePods {
 				ds.PodUpdateOrAddIfNotExist(pod)
@@ -683,10 +621,6 @@ func TestGetRandomPod(t *testing.T) {
 	}
 }
 
-func pointer(v int32) *int32 {
-	return &v
-}
-
 func TestDirector_HandleResponse(t *testing.T) {
 	pr1 := newTestPostResponse("pr1")
 
diff --git a/pkg/epp/saturationdetector/saturationdetector_test.go b/pkg/epp/saturationdetector/saturationdetector_test.go
index c0ed3d2..f2530bd 100644
--- a/pkg/epp/saturationdetector/saturationdetector_test.go
+++ b/pkg/epp/saturationdetector/saturationdetector_test.go
@@ -114,6 +114,7 @@ func TestNewDetector(t *testing.T) {
 			detector := NewDetector(LoadConfigFromEnv(), test.datastore, logr.Discard())
 			if detector == nil {
 				t.Fatalf("NewDetector() returned nil detector for valid config")
+				return // explicit return so staticcheck does not warn on potential nil detector below.
 			}
 			if detector.config.QueueDepthThreshold != test.expectedQueueDepthThreshold {
 				t.Errorf("NewDetector() QueueDepthThreshold = %d, want %d", detector.config.QueueDepthThreshold, test.expectedQueueDepthThreshold)
diff --git a/pkg/epp/scheduling/framework/plugins/test/consts.go b/pkg/epp/scheduling/framework/plugins/test/consts.go
new file mode 100644
index 0000000..81c0579
--- /dev/null
+++ b/pkg/epp/scheduling/framework/plugins/test/consts.go
@@ -0,0 +1,25 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+const (
+	// HeaderTestEppEndPointSelectionKey is the header used for testing purposes to make EPP behavior controllable.
+	// The header value should be a comma-separated list of endpoint IP addresses.
+	// E.g., "test-epp-endpoint-selection": "10.0.0.7,10.0.0.8"
+	// The returned order is the same as the order provided in the header.
+	HeaderTestEppEndPointSelectionKey = "test-epp-endpoint-selection"
+)
diff --git a/pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go b/pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go
index b7434be..d5615bc 100644
--- a/pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go
+++ b/pkg/epp/scheduling/framework/plugins/test/filter/filter_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/google/go-cmp/cmp"
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
@@ -47,7 +48,7 @@ func TestFilter(t *testing.T) {
 		},
 		{
 			name: "TestHeaderBasedFilter, header endpoint set in request but no match",
-			req:  &types.LLMRequest{Headers: map[string]string{HeaderTestEppEndPointSelectionKey: "test-endpoint"}},
+			req:  &types.LLMRequest{Headers: map[string]string{test.HeaderTestEppEndPointSelectionKey: "test-endpoint"}},
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
@@ -59,7 +60,7 @@ func TestFilter(t *testing.T) {
 		},
 		{
 			name: "TestHeaderBasedFilter, header endpoint set",
-			req:  &types.LLMRequest{Headers: map[string]string{HeaderTestEppEndPointSelectionKey: "test-endpoint"}},
+			req:  &types.LLMRequest{Headers: map[string]string{test.HeaderTestEppEndPointSelectionKey: "test-endpoint"}},
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
@@ -77,7 +78,7 @@ func TestFilter(t *testing.T) {
 		},
 		{
 			name: "TestHeaderBasedFilter, multiple header endpoints set and multiple matches",
-			req:  &types.LLMRequest{Headers: map[string]string{HeaderTestEppEndPointSelectionKey: "test-endpoint3,test-endpoint2"}},
+			req:  &types.LLMRequest{Headers: map[string]string{test.HeaderTestEppEndPointSelectionKey: "test-endpoint3,test-endpoint2"}},
 			input: []types.Pod{
 				&types.PodMetrics{
 					Pod: &backend.Pod{
diff --git a/pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go b/pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go
index 113ec7b..bf36d77 100644
--- a/pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go
+++ b/pkg/epp/scheduling/framework/plugins/test/filter/request_header_based_filter.go
@@ -23,15 +23,11 @@ import (
 
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/test"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 )
 
 const (
-	// HeaderTestEppEndPointSelectionKey is the header used for testing purposes to make EPP behavior controllable.
-	// The header value should be a comma-separated list of endpoint IP addresses.
-	// E.g., "test-epp-endpoint-selection": "10.0.0.7,10.0.0.8"
-	// The returned order is the same as the order provided in the header.
-	HeaderTestEppEndPointSelectionKey = "test-epp-endpoint-selection"
 	// HeaderBasedTestingFilterType is the filter type that is used in plugins registry.
 	HeaderBasedTestingFilterType = "header-based-testing-filter"
 )
@@ -70,7 +66,7 @@ func (f *HeaderBasedTestingFilter) WithName(name string) *HeaderBasedTestingFilt
 
 // Filter selects pods that match the IP addresses specified in the request header.
 func (f *HeaderBasedTestingFilter) Filter(_ context.Context, _ *types.CycleState, request *types.LLMRequest, pods []types.Pod) []types.Pod {
-	headerValue, ok := request.Headers[HeaderTestEppEndPointSelectionKey]
+	headerValue, ok := request.Headers[test.HeaderTestEppEndPointSelectionKey]
 	if !ok || headerValue == "" {
 		return []types.Pod{}
 	}
diff --git a/pkg/epp/util/testing/wrappers.go b/pkg/epp/util/testing/wrappers.go
index ce1b24b..9eeb3ad 100644
--- a/pkg/epp/util/testing/wrappers.go
+++ b/pkg/epp/util/testing/wrappers.go
@@ -136,18 +136,18 @@ func (m *InferenceObjectiveWrapper) ObjRef() *v1alpha2.InferenceObjective {
 	return &m.InferenceObjective
 }
 
-func (m *InferenceObjectiveWrapper) TargetModel(modelName string) *InferenceObjectiveWrapper {
-	m.Spec.TargetModels = append(m.Spec.TargetModels, v1alpha2.TargetModel{Name: modelName})
+func (m *InferenceObjectiveWrapper) PoolName(poolName string) *InferenceObjectiveWrapper {
+	m.Spec.PoolRef.Name = v1alpha2.ObjectName(poolName)
 	return m
 }
 
-func (m *InferenceObjectiveWrapper) PoolName(poolName string) *InferenceObjectiveWrapper {
-	m.Spec.PoolRef = v1alpha2.PoolObjectReference{Name: v1alpha2.ObjectName(poolName)}
+func (m *InferenceObjectiveWrapper) PoolGroup(poolGroup string) *InferenceObjectiveWrapper {
+	m.Spec.PoolRef.Group = v1alpha2.Group(poolGroup)
 	return m
 }
 
-func (m *InferenceObjectiveWrapper) Criticality(criticality v1alpha2.Criticality) *InferenceObjectiveWrapper {
-	m.Spec.Criticality = &criticality
+func (m *InferenceObjectiveWrapper) Priority(priority int) *InferenceObjectiveWrapper {
+	m.Spec.Priority = &priority
 	return m
 }
 
@@ -175,6 +175,10 @@ func MakeInferencePool(name string) *InferencePoolWrapper {
 			ObjectMeta: metav1.ObjectMeta{
 				Name: name,
 			},
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: "inference.networking.k8s.io/v1",
+				Kind:       "InferencePool",
+			},
 			Spec: v1.InferencePoolSpec{},
 		},
 	}
@@ -190,17 +194,19 @@ func (m *InferencePoolWrapper) Selector(selector map[string]string) *InferencePo
 	for k, v := range selector {
 		s[v1.LabelKey(k)] = v1.LabelValue(v)
 	}
-	m.Spec.Selector = s
+	m.Spec.Selector = v1.LabelSelector{
+		MatchLabels: s,
+	}
 	return m
 }
 
-func (m *InferencePoolWrapper) TargetPortNumber(p int32) *InferencePoolWrapper {
-	m.Spec.TargetPortNumber = p
+func (m *InferencePoolWrapper) TargetPorts(p int32) *InferencePoolWrapper {
+	m.Spec.TargetPorts = []v1.Port{{Number: v1.PortNumber(p)}}
 	return m
 }
 
 func (m *InferencePoolWrapper) ExtensionRef(name string) *InferencePoolWrapper {
-	m.Spec.ExtensionRef = &v1.Extension{ExtensionReference: v1.ExtensionReference{Name: v1.ObjectName(name)}}
+	m.Spec.ExtensionRef = v1.Extension{Name: v1.ObjectName(name)}
 	return m
 }
 
@@ -209,14 +215,14 @@ func (m *InferencePoolWrapper) ObjRef() *v1.InferencePool {
 	return &m.InferencePool
 }
 
-// XInferencePoolWrapper wraps an group "inference.networking.x-k8s.io" InferencePool.
-type XInferencePoolWrapper struct {
+// AlphaInferencePoolWrapper wraps an group "inference.networking.x-k8s.io" InferencePool.
+type AlphaInferencePoolWrapper struct {
 	v1alpha2.InferencePool
 }
 
-// MakeXInferencePool creates a wrapper for a InferencePool.
-func MakeXInferencePool(name string) *XInferencePoolWrapper {
-	return &XInferencePoolWrapper{
+// MakeAlphaInferencePool creates a wrapper for a InferencePool.
+func MakeAlphaInferencePool(name string) *AlphaInferencePoolWrapper {
+	return &AlphaInferencePoolWrapper{
 		v1alpha2.InferencePool{
 			ObjectMeta: metav1.ObjectMeta{
 				Name: name,
@@ -226,12 +232,12 @@ func MakeXInferencePool(name string) *XInferencePoolWrapper {
 	}
 }
 
-func (m *XInferencePoolWrapper) Namespace(ns string) *XInferencePoolWrapper {
+func (m *AlphaInferencePoolWrapper) Namespace(ns string) *AlphaInferencePoolWrapper {
 	m.ObjectMeta.Namespace = ns
 	return m
 }
 
-func (m *XInferencePoolWrapper) Selector(selector map[string]string) *XInferencePoolWrapper {
+func (m *AlphaInferencePoolWrapper) Selector(selector map[string]string) *AlphaInferencePoolWrapper {
 	s := make(map[v1alpha2.LabelKey]v1alpha2.LabelValue)
 	for k, v := range selector {
 		s[v1alpha2.LabelKey(k)] = v1alpha2.LabelValue(v)
@@ -240,17 +246,17 @@ func (m *XInferencePoolWrapper) Selector(selector map[string]string) *XInference
 	return m
 }
 
-func (m *XInferencePoolWrapper) TargetPortNumber(p int32) *XInferencePoolWrapper {
+func (m *AlphaInferencePoolWrapper) TargetPortNumber(p int32) *AlphaInferencePoolWrapper {
 	m.Spec.TargetPortNumber = p
 	return m
 }
 
-func (m *XInferencePoolWrapper) ExtensionRef(name string) *XInferencePoolWrapper {
-	m.Spec.ExtensionRef = &v1alpha2.Extension{ExtensionReference: v1alpha2.ExtensionReference{Name: v1alpha2.ObjectName(name)}}
+func (m *AlphaInferencePoolWrapper) ExtensionRef(name string) *AlphaInferencePoolWrapper {
+	m.Spec.ExtensionRef = v1alpha2.Extension{Name: v1alpha2.ObjectName(name)}
 	return m
 }
 
 // Obj returns the wrapped InferencePool.
-func (m *XInferencePoolWrapper) ObjRef() *v1alpha2.InferencePool {
+func (m *AlphaInferencePoolWrapper) ObjRef() *v1alpha2.InferencePool {
 	return &m.InferencePool
 }
diff --git a/test/e2e/epp/README.md b/test/e2e/epp/README.md
index c3e4aa1..5a83f06 100644
--- a/test/e2e/epp/README.md
+++ b/test/e2e/epp/README.md
@@ -45,6 +45,19 @@ Follow these steps to run the end-to-end tests:
      export E2E_MANIFEST_PATH=[config/manifests/vllm/gpu-deployment.yaml|config/manifests/vllm/cpu-deployment.yaml]
      ```
 
+   - **Enable leader election tests**: By default, the e2e test runs the EPP server as a single replica.
+     To test the high-availability (HA) mode with leader election (3 replicas), set the following environment variable:
+
+     ```sh
+     export E2E_LEADER_ELECTION_ENABLED=true
+     ```
+
+   - **Pause before cleanup**: To pause the test run before cleaning up resources, set the `E2E_PAUSE_ON_EXIT` environment variable.
+     This is useful for debugging the state of the cluster after the test has run.
+
+     - To pause indefinitely, set it to `true`: `export E2E_PAUSE_ON_EXIT=true`
+     - To pause for a specific duration, provide a duration string: `export E2E_PAUSE_ON_EXIT=10m`
+
 1. **Run the Tests**: Run the `test-e2e` target:
 
    ```sh
diff --git a/test/e2e/epp/e2e_suite_test.go b/test/e2e/epp/e2e_suite_test.go
index a40d586..e6e5b83 100644
--- a/test/e2e/epp/e2e_suite_test.go
+++ b/test/e2e/epp/e2e_suite_test.go
@@ -85,8 +85,10 @@ const (
 	xInferObjectiveManifest = "../../../config/crd/bases/inference.networking.x-k8s.io_inferenceobjectives.yaml"
 	// inferPoolManifest is the manifest for the inference pool CRD with 'inference.networking.k8s.io' group.
 	inferPoolManifest = "../../../config/crd/bases/inference.networking.k8s.io_inferencepools.yaml"
-	// inferExtManifest is the manifest for the inference extension test resources.
-	inferExtManifest = "../../testdata/inferencepool-e2e.yaml"
+	// inferExtManifestDefault is the manifest for the default inference extension test resources (single replica).
+	inferExtManifestDefault = "../../testdata/inferencepool-e2e.yaml"
+	// inferExtManifestLeaderElection is the manifest for the inference extension test resources with leader election enabled (3 replicas).
+	inferExtManifestLeaderElection = "../../testdata/inferencepool-leader-election-e2e.yaml"
 	// envoyManifest is the manifest for the envoy proxy test resources.
 	envoyManifest = "../../testdata/envoy.yaml"
 	// metricsRbacManifest is the manifest for the rbac resources for testing metrics.
@@ -95,15 +97,18 @@ const (
 	modelServerManifestFilepathEnvVar = "MANIFEST_PATH"
 )
 
+const e2eLeaderElectionEnabledEnvVar = "E2E_LEADER_ELECTION_ENABLED"
+
 var (
 	ctx = context.Background()
 	cli client.Client
 	// Required for exec'ing in curl pod
-	kubeCli  *kubernetes.Clientset
-	scheme   = runtime.NewScheme()
-	cfg      = config.GetConfigOrDie()
-	nsName   string
-	e2eImage string
+	kubeCli               *kubernetes.Clientset
+	scheme                = runtime.NewScheme()
+	cfg                   = config.GetConfigOrDie()
+	nsName                string
+	e2eImage              string
+	leaderElectionEnabled bool
 )
 
 func TestAPIs(t *testing.T) {
@@ -121,6 +126,11 @@ var _ = ginkgo.BeforeSuite(func() {
 	e2eImage = os.Getenv("E2E_IMAGE")
 	gomega.Expect(e2eImage).NotTo(gomega.BeEmpty(), "E2E_IMAGE environment variable is not set")
 
+	if os.Getenv(e2eLeaderElectionEnabledEnvVar) == "true" {
+		leaderElectionEnabled = true
+		ginkgo.By("Leader election test mode enabled via " + e2eLeaderElectionEnabledEnvVar)
+	}
+
 	ginkgo.By("Setting up the test suite")
 	setupSuite()
 
@@ -146,7 +156,12 @@ func setupInfra() {
 	}
 
 	createCRDs(cli, crds)
-	createInferExt(cli, inferExtManifest)
+
+	inferExtManifestPath := inferExtManifestDefault
+	if leaderElectionEnabled {
+		inferExtManifestPath = inferExtManifestLeaderElection
+	}
+	createInferExt(cli, inferExtManifestPath)
 	createClient(cli, clientManifest)
 	createEnvoy(cli, envoyManifest)
 	createMetricsRbac(cli, metricsRbacManifest)
@@ -156,6 +171,20 @@ func setupInfra() {
 }
 
 var _ = ginkgo.AfterSuite(func() {
+	// If E2E_PAUSE_ON_EXIT is set, pause the test run before cleanup.
+	// This is useful for debugging the state of the cluster after the test has run.
+	if pauseStr := os.Getenv("E2E_PAUSE_ON_EXIT"); pauseStr != "" {
+		ginkgo.By("Pausing before cleanup as requested by E2E_PAUSE_ON_EXIT=" + pauseStr)
+		pauseDuration, err := time.ParseDuration(pauseStr)
+		if err != nil {
+			// If it's not a valid duration (e.g., "true"), just wait indefinitely.
+			ginkgo.By("Invalid duration, pausing indefinitely. Press Ctrl+C to stop the test runner when you are done.")
+			select {} // Block forever
+		}
+		ginkgo.By(fmt.Sprintf("Pausing for %v...", pauseDuration))
+		time.Sleep(pauseDuration)
+	}
+
 	ginkgo.By("Performing global cleanup")
 	cleanupResources()
 })
@@ -423,8 +452,12 @@ func createInferExt(k8sClient client.Client, filePath string) {
 		return k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, deploy)
 	}, existsTimeout, interval)
 
-	// Wait for the deployment to be available.
-	testutils.DeploymentAvailable(ctx, k8sClient, deploy, modelReadyTimeout, interval)
+	if leaderElectionEnabled {
+		// With leader election enabled, only 1 replica will be "Ready" at any given time (the leader).
+		testutils.DeploymentReadyReplicas(ctx, k8sClient, deploy, 1, modelReadyTimeout, interval)
+	} else {
+		testutils.DeploymentAvailable(ctx, k8sClient, deploy, modelReadyTimeout, interval)
+	}
 
 	// Wait for the service to exist.
 	testutils.EventuallyExists(ctx, func() error {
diff --git a/test/e2e/epp/e2e_test.go b/test/e2e/epp/e2e_test.go
index 5e9b408..e01240d 100644
--- a/test/e2e/epp/e2e_test.go
+++ b/test/e2e/epp/e2e_test.go
@@ -28,10 +28,10 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/onsi/ginkgo/v2"
 	"github.com/onsi/gomega"
+	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
@@ -72,181 +72,246 @@ var _ = ginkgo.Describe("InferencePool", func() {
 
 	ginkgo.When("The Inference Extension is running", func() {
 		ginkgo.It("Should route traffic to target model servers", func() {
-			for _, t := range []struct {
-				api              string
-				promptOrMessages any
-			}{
-				{
-					api:              "/completions",
-					promptOrMessages: "Write as if you were a critic: San Francisco",
-				},
-				{
-					api: "/chat/completions",
-					promptOrMessages: []map[string]any{
-						{
-							"role":    "user",
-							"content": "Write as if you were a critic: San Francisco",
-						},
-					},
-				},
-				{
-					api: "/chat/completions",
-					promptOrMessages: []map[string]any{
-						{
-							"role":    "user",
-							"content": "Write as if you were a critic: San Francisco",
-						},
-						{"role": "assistant", "content": "Okay, let's see..."},
-						{"role": "user", "content": "Now summarize your thoughts."},
-					},
-				},
-			} {
-				ginkgo.By(fmt.Sprintf("Verifying connectivity through the inference extension with %s api and prompt/messages: %v", t.api, t.promptOrMessages))
-
-				// Ensure the expected responses include the InferenceObjective target model names.
-				var expected []string
-				for _, m := range infObjective.Spec.TargetModels {
-					expected = append(expected, m.Name)
-				}
-				curlCmd := getCurlCommand(envoyName, nsName, envoyPort, modelName, curlTimeout, t.api, t.promptOrMessages, false)
-
-				actual := make(map[string]int)
-				gomega.Eventually(func() error {
-					resp, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd)
-					if err != nil {
-						return err
-					}
-					if !strings.Contains(resp, "200 OK") {
-						return fmt.Errorf("did not get 200 OK: %s", resp)
-					}
-					for _, m := range expected {
-						if strings.Contains(resp, m) {
-							actual[m] = 0
-						}
-					}
-					var got []string
-					for m := range actual {
-						got = append(got, m)
-					}
-					// Compare ignoring order
-					if !cmp.Equal(got, expected, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
-						return fmt.Errorf("actual (%v) != expected (%v); resp=%q", got, expected, resp)
-					}
-					return nil
-				}, readyTimeout, curlInterval).Should(gomega.Succeed())
-			}
+			verifyTrafficRouting()
 		})
 
 		ginkgo.It("Should expose EPP metrics after generating traffic", func() {
-			// Define the metrics we expect to see
-			expectedMetrics := []string{
-				"inference_model_request_total",
-				"inference_model_request_error_total",
-				"inference_model_request_duration_seconds",
-				// TODO: normalized_time_per_output_token_seconds is not actually recorded yet
-				// "normalized_time_per_output_token_seconds",
-				"inference_model_request_sizes",
-				"inference_model_response_sizes",
-				"inference_model_input_tokens",
-				"inference_model_output_tokens",
-				"inference_pool_average_kv_cache_utilization",
-				"inference_pool_average_queue_size",
-				"inference_pool_per_pod_queue_size",
-				"inference_model_running_requests",
-				"inference_pool_ready_pods",
-				"inference_extension_info",
+			verifyMetrics()
+		})
+	})
+
+	ginkgo.When("Leader election is enabled", func() {
+		ginkgo.It("Should elect one leader and have other pods as not ready", func() {
+			if !leaderElectionEnabled {
+				ginkgo.Skip("Leader election is not enabled for this test run, skipping.")
 			}
 
-			// Generate traffic by sending requests through the inference extension
-			ginkgo.By("Generating traffic through the inference extension")
-			curlCmd := getCurlCommand(envoyName, nsName, envoyPort, modelName, curlTimeout, "/completions", "Write as if you were a critic: San Francisco", true)
+			ginkgo.By("Verifying that exactly one EPP pod is ready")
+			gomega.Eventually(func(g gomega.Gomega) {
+				podList := &corev1.PodList{}
+				err := cli.List(ctx, podList, client.InNamespace(nsName), client.MatchingLabels{"app": inferExtName})
+				g.Expect(err).NotTo(gomega.HaveOccurred())
 
-			// Run the curl command multiple times to generate some metrics data
-			for i := 0; i < 5; i++ {
-				_, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd)
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
-			}
+				// The deployment should have 3 replicas for leader election.
+				g.Expect(podList.Items).To(gomega.HaveLen(3))
 
-			// modify the curl command to generate some error metrics
-			curlCmd[len(curlCmd)-1] = "invalid input"
-			for i := 0; i < 5; i++ {
-				_, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd)
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				readyPods := 0
+				for _, pod := range podList.Items {
+					for _, cond := range pod.Status.Conditions {
+						if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
+							readyPods++
+						}
+					}
+				}
+				g.Expect(readyPods).To(gomega.Equal(1), "Expected exactly one pod to be ready")
+			}, readyTimeout, interval).Should(gomega.Succeed())
+		})
+
+		ginkgo.It("Should successfully failover and serve traffic after the leader pod is deleted", func() {
+			if !leaderElectionEnabled {
+				ginkgo.Skip("Leader election is not enabled for this test run, skipping.")
 			}
 
-			// Now scrape metrics from the EPP endpoint via the curl pod
-			ginkgo.By("Scraping metrics from the EPP endpoint")
+			ginkgo.By("STEP 1: Verifying initial leader is working correctly before failover")
+			verifyTrafficRouting()
+			verifyMetrics()
+
+			ginkgo.By("STEP 2: Finding and deleting the current leader pod")
+			oldLeaderPod := findReadyPod()
+			ginkgo.By("Found initial leader pod: " + oldLeaderPod.Name)
 
-			// Get Pod IP instead of Service
-			podList := &corev1.PodList{}
-			err := cli.List(ctx, podList, client.InNamespace(nsName), client.MatchingLabels{"app": inferExtName})
-			gomega.Expect(err).NotTo(gomega.HaveOccurred())
-			gomega.Expect(podList.Items).NotTo(gomega.BeEmpty())
-			podIP := podList.Items[0].Status.PodIP
-			gomega.Expect(podIP).NotTo(gomega.BeEmpty())
+			ginkgo.By(fmt.Sprintf("Deleting leader pod %s to trigger failover", oldLeaderPod.Name))
+			gomega.Expect(cli.Delete(ctx, oldLeaderPod)).To(gomega.Succeed())
 
-			// Get the authorization token for reading metrics
-			token := ""
+			ginkgo.By("STEP 3: Waiting for a new leader to be elected")
+			// The deployment controller will create a new pod. We need to wait for the total number of pods
+			// to be back to 3, and for one of the other pods to become the new leader.
+			deploy := &appsv1.Deployment{}
 			gomega.Eventually(func() error {
-				token, err = getMetricsReaderToken(cli)
-				if err != nil {
-					return err
-				}
-				if token == "" {
-					return errors.New("token not found")
-				}
-				return nil
+				return cli.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, deploy)
 			}, existsTimeout, interval).Should(gomega.Succeed())
 
-			// Construct the metric scraping curl command using Pod IP
-			metricScrapeCmd := []string{
-				"curl",
-				"-i",
-				"--max-time",
-				strconv.Itoa((int)(curlTimeout.Seconds())),
-				"-H",
-				"Authorization: Bearer " + token,
-				fmt.Sprintf("http://%s:%d/metrics", podIP, 9090),
-			}
+			// Wait for one replica to become ready again.
+			testutils.DeploymentReadyReplicas(ctx, cli, deploy, 1, readyTimeout, interval)
 
-			ginkgo.By("Verifying that all expected metrics are present.")
-			gomega.Eventually(func() error {
-				// Execute the metrics scrape command inside the curl pod
-				resp, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", metricScrapeCmd)
-				if err != nil {
-					return err
-				}
-				// Verify that we got a 200 OK responsecurl
-				if !strings.Contains(resp, "200 OK") {
-					return fmt.Errorf("did not get 200 OK: %s", resp)
-				}
-				// Check if all expected metrics are present in the metrics output
-				for _, metric := range expectedMetrics {
-					if !strings.Contains(resp, metric) {
-						return fmt.Errorf("expected metric %s not found in metrics output", metric)
-					}
-				}
-				return nil
-			}, readyTimeout, curlInterval).Should(gomega.Succeed())
+			// Also wait for the total number of replicas to be back to 3.
+			gomega.Eventually(func(g gomega.Gomega) {
+				d := &appsv1.Deployment{}
+				err := cli.Get(ctx, types.NamespacedName{Namespace: nsName, Name: inferExtName}, d)
+				g.Expect(err).NotTo(gomega.HaveOccurred())
+				g.Expect(d.Status.Replicas).To(gomega.Equal(int32(3)), "Deployment should have 3 replicas")
+			}, readyTimeout, interval).Should(gomega.Succeed())
+
+			ginkgo.By("STEP 4: Verifying a new, different leader is elected")
+			var newLeaderPod *corev1.Pod
+			gomega.Eventually(func(g gomega.Gomega) {
+				// Find the current ready pod.
+				newLeaderPod = findReadyPod()
+
+				// Ensure the new leader is not the same as the one we just deleted.
+				// This guards against a race condition where we might find the old leader
+				// before its status is updated to NotReady.
+				g.Expect(newLeaderPod.Name).NotTo(gomega.Equal(oldLeaderPod.Name), "The new leader should not be the same as the old deleted leader")
+			}, readyTimeout, interval).Should(gomega.Succeed())
+			ginkgo.By("Found new leader pod: " + newLeaderPod.Name)
+
+			ginkgo.By("STEP 5: Verifying the new leader is working correctly after failover")
+			verifyTrafficRouting()
+			verifyMetrics()
 		})
 	})
 })
 
 // newInferenceObjective creates an InferenceObjective in the given namespace for testutils.
 func newInferenceObjective(ns string) *v1alpha2.InferenceObjective {
-	targets := []v1alpha2.TargetModel{
-		{
-			Name:   targetModelName,
-			Weight: ptr.To(int32(100)),
-		},
-	}
 	return testutils.MakeModelWrapper(types.NamespacedName{Name: "inferenceobjective-sample", Namespace: ns}).
-		SetCriticality(v1alpha2.Critical).
+		SetPriority(2).
 		SetPoolRef(modelServerName).
-		SetTargetModels(targets).
 		Obj()
 }
 
+// verifyTrafficRouting contains the logic for the "Should route traffic to target model servers" test.
+func verifyTrafficRouting() {
+	ginkgo.By("Verifying traffic routing")
+	for _, t := range []struct {
+		api              string
+		promptOrMessages any
+	}{
+		{
+			api:              "/completions",
+			promptOrMessages: "Write as if you were a critic: San Francisco",
+		},
+		{
+			api: "/chat/completions",
+			promptOrMessages: []map[string]any{
+				{
+					"role":    "user",
+					"content": "Write as if you were a critic: San Francisco",
+				},
+			},
+		},
+		{
+			api: "/chat/completions",
+			promptOrMessages: []map[string]any{
+				{
+					"role":    "user",
+					"content": "Write as if you were a critic: San Francisco",
+				},
+				{"role": "assistant", "content": "Okay, let's see..."},
+				{"role": "user", "content": "Now summarize your thoughts."},
+			},
+		},
+	} {
+		ginkgo.By(fmt.Sprintf("Verifying connectivity through the inference extension with %s api and prompt/messages: %v", t.api, t.promptOrMessages))
+
+		// Ensure the expected responses include the InferenceObjective target model names.
+		var expected []string
+		expected = append(expected, targetModelName)
+		curlCmd := getCurlCommand(envoyName, nsName, envoyPort, modelName, curlTimeout, t.api, t.promptOrMessages, false)
+
+		actual := make(map[string]int)
+		gomega.Eventually(func() error {
+			resp, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd)
+			if err != nil {
+				return err
+			}
+			if !strings.Contains(resp, "200 OK") {
+				return fmt.Errorf("did not get 200 OK: %s", resp)
+			}
+			for _, m := range expected {
+				if strings.Contains(resp, m) {
+					actual[m] = 0
+				}
+			}
+			var got []string
+			for m := range actual {
+				got = append(got, m)
+			}
+			// Compare ignoring order
+			if !cmp.Equal(got, expected, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
+				return fmt.Errorf("actual (%v) != expected (%v); resp=%q", got, expected, resp)
+			}
+			return nil
+		}, readyTimeout, curlInterval).Should(gomega.Succeed())
+	}
+}
+
+// verifyMetrics contains the logic for the "Should expose EPP metrics after generating traffic" test.
+func verifyMetrics() {
+	ginkgo.By("Verifying metrics exposure")
+	// Define the metrics we expect to see
+	expectedMetrics := []string{
+		"inference_model_request_total",
+		"inference_model_request_error_total",
+		"inference_model_request_duration_seconds",
+		// TODO: normalized_time_per_output_token_seconds is not actually recorded yet
+		// "normalized_time_per_output_token_seconds",
+		"inference_model_request_sizes",
+		"inference_model_response_sizes",
+		"inference_model_input_tokens",
+		"inference_model_output_tokens",
+		"inference_pool_average_kv_cache_utilization",
+		"inference_pool_average_queue_size",
+		"inference_pool_per_pod_queue_size",
+		"inference_model_running_requests",
+		"inference_pool_ready_pods",
+		"inference_extension_info",
+	}
+
+	// Generate traffic by sending requests through the inference extension
+	ginkgo.By("Generating traffic through the inference extension")
+	curlCmd := getCurlCommand(envoyName, nsName, envoyPort, modelName, curlTimeout, "/completions", "Write as if you were a critic: San Francisco", true)
+
+	// Run the curl command multiple times to generate some metrics data
+	for i := 0; i < 5; i++ {
+		_, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd)
+		gomega.Expect(err).NotTo(gomega.HaveOccurred())
+	}
+
+	// modify the curl command to generate some error metrics
+	curlCmd[len(curlCmd)-1] = "invalid input"
+	for i := 0; i < 5; i++ {
+		_, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", curlCmd)
+		gomega.Expect(err).NotTo(gomega.HaveOccurred())
+	}
+
+	// Now scrape metrics from the EPP endpoint via the curl pod
+	ginkgo.By("Scraping metrics from the EPP endpoint")
+	podIP := findReadyPod().Status.PodIP
+
+	// Get the authorization token for reading metrics
+	token := ""
+	gomega.Eventually(func(g gomega.Gomega) {
+		t, err := getMetricsReaderToken(cli)
+		g.Expect(err).NotTo(gomega.HaveOccurred())
+		g.Expect(t).NotTo(gomega.BeEmpty())
+		token = t
+	}, existsTimeout, interval).Should(gomega.Succeed())
+
+	// Construct the metric scraping curl command using Pod IP
+	metricScrapeCmd := getMetricsScrapeCommand(podIP, token)
+
+	ginkgo.By("Verifying that all expected metrics are present.")
+	gomega.Eventually(func() error {
+		// Execute the metrics scrape command inside the curl pod
+		resp, err := testutils.ExecCommandInPod(ctx, cfg, scheme, kubeCli, nsName, "curl", "curl", metricScrapeCmd)
+		if err != nil {
+			return err
+		}
+		// Verify that we got a 200 OK responsecurl
+		if !strings.Contains(resp, "200 OK") {
+			return fmt.Errorf("did not get 200 OK: %s", resp)
+		}
+		// Check if all expected metrics are present in the metrics output
+		for _, metric := range expectedMetrics {
+			if !strings.Contains(resp, metric) {
+				return fmt.Errorf("expected metric %s not found in metrics output", metric)
+			}
+		}
+		return nil
+	}, readyTimeout, curlInterval).Should(gomega.Succeed())
+}
+
 func getMetricsReaderToken(k8sClient client.Client) (string, error) {
 	secret := &corev1.Secret{}
 	err := k8sClient.Get(ctx, types.NamespacedName{Namespace: nsName, Name: metricsReaderSecretName}, secret)
@@ -256,6 +321,43 @@ func getMetricsReaderToken(k8sClient client.Client) (string, error) {
 	return string(secret.Data["token"]), nil
 }
 
+// findReadyPod finds the first EPP pod that has a "Ready" status condition.
+// It's used to target the leader pod in an HA setup.
+func findReadyPod() *corev1.Pod {
+	var readyPod *corev1.Pod
+	gomega.Eventually(func(g gomega.Gomega) {
+		podList := &corev1.PodList{}
+		err := cli.List(ctx, podList, client.InNamespace(nsName), client.MatchingLabels{"app": inferExtName})
+		g.Expect(err).NotTo(gomega.HaveOccurred())
+
+		foundReadyPod := false
+		for i := range podList.Items {
+			pod := &podList.Items[i]
+			for _, cond := range pod.Status.Conditions {
+				if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
+					g.Expect(pod.Status.PodIP).NotTo(gomega.BeEmpty(), "Ready pod must have an IP")
+					readyPod = pod
+					foundReadyPod = true
+					break // break inner loop
+				}
+			}
+			if foundReadyPod {
+				break // break outer loop
+			}
+		}
+		g.Expect(foundReadyPod).To(gomega.BeTrue(), "No ready EPP pod found")
+	}, readyTimeout, interval).Should(gomega.Succeed())
+	return readyPod
+}
+
+// getMetricsScrapeCommand returns the command to scrape the /metrics endpoint.
+func getMetricsScrapeCommand(podIP, token string) []string {
+	return []string{
+		"curl", "-i", "--max-time", strconv.Itoa((int)(curlTimeout.Seconds())),
+		"-H", "Authorization: Bearer " + token, fmt.Sprintf("http://%s:%d/metrics", podIP, 9090),
+	}
+}
+
 // getCurlCommand returns the command, as a slice of strings, for curl'ing
 // the test model server at the given name, namespace, port, and model name.
 func getCurlCommand(name, ns, port, model string, timeout time.Duration, api string, promptOrMessages any, streaming bool) []string {
@@ -289,6 +391,8 @@ func getCurlCommand(name, ns, port, model string, timeout time.Duration, api str
 		"Content-Type: application/json",
 		"-H",
 		fmt.Sprintf("%v: inferenceobjective-sample", metadata.ObjectiveKey),
+		"-H",
+		fmt.Sprintf("%v: %s", metadata.ModelNameRewriteKey, targetModelName),
 		"-d",
 		string(b),
 	}
diff --git a/test/integration/bbr/hermetic_test.go b/test/integration/bbr/hermetic_test.go
index 69654be..e1c25a7 100644
--- a/test/integration/bbr/hermetic_test.go
+++ b/test/integration/bbr/hermetic_test.go
@@ -108,7 +108,7 @@ func TestFullDuplexStreamed_BodyBasedRouting(t *testing.T) {
 	}{
 		{
 			name: "success adding model parameter to header",
-			reqs: integrationutils.GenerateStreamedRequestSet(logger, "test", "foo", nil),
+			reqs: integrationutils.GenerateStreamedRequestSet(logger, "test", "foo", "foo", nil),
 			wantResponses: []*extProcPb.ProcessingResponse{
 				{
 					Response: &extProcPb.ProcessingResponse_RequestHeaders{
@@ -213,7 +213,7 @@ func TestFullDuplexStreamed_BodyBasedRouting(t *testing.T) {
 		},
 		{
 			name: "no model parameter",
-			reqs: integrationutils.GenerateStreamedRequestSet(logger, "test", "", nil),
+			reqs: integrationutils.GenerateStreamedRequestSet(logger, "test", "", "", nil),
 			wantResponses: []*extProcPb.ProcessingResponse{
 				{
 					Response: &extProcPb.ProcessingResponse_RequestHeaders{
diff --git a/test/integration/epp/hermetic_test.go b/test/integration/epp/hermetic_test.go
index c82a702..48cf889 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -161,7 +161,7 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 		// Request flow tests
 		{
 			name:     "select lower queue and kv cache, no active lora",
-			requests: integrationutils.GenerateStreamedRequestSet(logger, "test1", modelMyModel, nil),
+			requests: integrationutils.GenerateStreamedRequestSet(logger, "test1", modelMyModel, modelMyModelTarget, nil),
 			// Pod 1 will be picked because it has relatively low queue size and low KV cache.
 			pods: newPodStates(
 				podState{index: 0, queueSize: 3, kvCacheUsage: 0.2},
@@ -226,7 +226,7 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 		},
 		{
 			name:     "select active lora, low queue",
-			requests: integrationutils.GenerateStreamedRequestSet(logger, "test2", modelSQLLora, nil),
+			requests: integrationutils.GenerateStreamedRequestSet(logger, "test2", modelSQLLora, modelSQLLoraTarget, nil),
 			// Pod 1 will be picked because it has relatively low queue size, the requested model active, and low KV cache.
 			pods: newPodStates(
 				podState{index: 0, queueSize: 0, kvCacheUsage: 0.2, activeModels: []string{"foo", "bar"}},
@@ -254,7 +254,7 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 		},
 		{
 			name:     "select lora despite higher kv cache usage",
-			requests: integrationutils.GenerateStreamedRequestSet(logger, "test3", modelSQLLora, nil),
+			requests: integrationutils.GenerateStreamedRequestSet(logger, "test3", modelSQLLora, modelSQLLoraTarget, nil),
 			// Pod 2 will be picked despite NOT having the requested model active as it is above the affinity for queue size.
 			// Also it is critical, so we should still admit the request despite all queue sizes being greater than the queue
 			// size threshold.
@@ -282,42 +282,26 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 			),
 		},
 		{
-			name:     "noncritical and all models past threshold, shed request",
-			requests: integrationutils.GenerateStreamedRequestSet(logger, "test4", modelSheddable, nil),
+			name:     "don't shed requests by default",
+			requests: integrationutils.GenerateStreamedRequestSet(logger, "test4", modelSQLLora, modelSQLLoraTarget, nil),
 			// pod 0: excluded; above queue size threshold
 			// pod 1: excluded; above KV cache threshold
 			// pod 2: excluded; above queue size threshold
 			pods: newPodStates(
-				podState{index: 0, queueSize: 6, kvCacheUsage: 0.2, activeModels: []string{"foo", "bar", modelSheddableTarget}},
-				podState{index: 1, queueSize: 0, kvCacheUsage: 0.85, activeModels: []string{"foo", modelSheddableTarget}},
-				podState{index: 2, queueSize: 10, kvCacheUsage: 0.9, activeModels: []string{"foo", modelSheddableTarget}},
-			),
-			wantErr:     false,
-			wantMetrics: map[string]string{},
-			wantResponses: integrationutils.NewImmediateErrorResponse(
-				envoyTypePb.StatusCode_TooManyRequests,
-				"inference gateway: InferencePoolResourceExhausted - system saturated, non-critical request dropped",
-			),
-		},
-		{
-			name:     "noncritical, but one server has capacity, do not shed",
-			requests: integrationutils.GenerateStreamedRequestSet(logger, "test5", modelSheddable, nil),
-			// Pod 1 will be picked because it has relatively low queue size and low KV cache.
-			pods: newPodStates(
-				podState{index: 0, queueSize: 4, kvCacheUsage: 0.2, activeModels: []string{"foo", "bar", modelSheddableTarget}},
-				podState{index: 1, queueSize: 4, kvCacheUsage: 0.85, activeModels: []string{"foo", modelSheddableTarget}},
-				podState{index: 2, queueSize: 10, kvCacheUsage: 0.9, activeModels: []string{"foo", modelSheddableTarget}},
+				podState{index: 0, queueSize: 6, kvCacheUsage: 0.2, activeModels: []string{"foo", "bar", modelSQLLoraTarget}},
+				podState{index: 1, queueSize: 0, kvCacheUsage: 0.85, activeModels: []string{"foo"}},
+				podState{index: 2, queueSize: 10, kvCacheUsage: 0.9, activeModels: []string{"foo"}},
 			),
 			wantMetrics: map[string]string{
 				"inference_model_request_total": inferenceObjectiveRequestTotal([]label{
-					{"model_name", modelSheddable},
-					{"target_model_name", modelSheddableTarget},
+					{"model_name", modelSQLLora},
+					{"target_model_name", modelSQLLoraTarget},
 				}),
 			},
 			wantErr: false,
 			wantResponses: integrationutils.NewRequestBufferedResponse(
 				"192.168.1.1:8000",
-				fmt.Sprintf(`{"max_tokens":100,"model":%q,"prompt":"test5","temperature":0}`, modelSheddableTarget),
+				fmt.Sprintf(`{"max_tokens":100,"model":%q,"prompt":"test4","temperature":0}`, modelSQLLoraTarget),
 				&configPb.HeaderValueOption{
 					Header: &configPb.HeaderValue{
 						Key:      "hi",
@@ -340,7 +324,11 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 									},
 									{
 										Key:   metadata.ObjectiveKey,
-										Value: "sql-lora-sheddable",
+										Value: modelSheddable,
+									},
+									{
+										Key:   metadata.ModelNameRewriteKey,
+										Value: modelSheddableTarget,
 									},
 								},
 							},
@@ -398,6 +386,14 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 										Key:   metadata.ObjectiveKey,
 										Value: modelDirect,
 									},
+									{
+										Key:   metadata.ModelNameRewriteKey,
+										Value: modelDirect,
+									},
+									{
+										Key:   metadata.ModelNameRewriteKey,
+										Value: modelDirect,
+									},
 								},
 							},
 						},
@@ -757,6 +753,7 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 				logger,
 				"test2",
 				modelSQLLora,
+				modelSQLLoraTarget,
 				[]string{"192.168.1.1:8000", "192.168.1.2:8000", "192.168.1.3:8000"}),
 			// Pod 1 will be picked because it has relatively low queue size, the requested model active, low KV cache, and within subset.
 			pods: newPodStates(
@@ -789,6 +786,7 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 				logger,
 				"test2",
 				modelSQLLora,
+				modelSQLLoraTarget,
 				[]string{"192.168.1.3:8000"}),
 			// Pod 3 has high queue and kv cache utilization, but it will still be picked because it is the only one matching subsetting target.
 			pods: newPodStates(
@@ -821,6 +819,7 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 				logger,
 				"test2",
 				modelSQLLora,
+				modelSQLLoraTarget,
 				[]string{"192.168.1.4:8000", "192.168.1.5:8000", "192.168.1.6:8000"}),
 			// No pods will be picked as none are within the subset.
 			pods: newPodStates(
@@ -986,7 +985,7 @@ func setUpHermeticServer(t *testing.T, podAndMetrics map[*backend.Pod]*backendme
 
 	// check if all pods are synced to datastore
 	assert.EventuallyWithT(t, func(t *assert.CollectT) {
-		assert.Len(t, serverRunner.Datastore.PodList(backendmetrics.AllPodPredicate), len(podAndMetrics), "Datastore not synced")
+		assert.Len(t, serverRunner.Datastore.PodList(backendmetrics.AllPodsPredicate), len(podAndMetrics), "Datastore not synced")
 	}, 10*time.Second, time.Second)
 
 	// Create a grpc connection
@@ -1095,7 +1094,7 @@ func BeforeSuite() func() {
 
 	serverRunner = server.NewDefaultExtProcServerRunner()
 	serverRunner.TestPodMetricsClient = &backendmetrics.FakePodMetricsClient{}
-	pmf := backendmetrics.NewPodMetricsFactory(serverRunner.TestPodMetricsClient, 10*time.Millisecond, time.Second*2)
+	pmf := backendmetrics.NewPodMetricsFactory(serverRunner.TestPodMetricsClient, 10*time.Millisecond)
 	// Adjust from defaults
 	serverRunner.PoolGKNN = common.GKNN{
 		NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPoolName},
diff --git a/test/integration/util.go b/test/integration/util.go
index 38ef0a1..0052449 100644
--- a/test/integration/util.go
+++ b/test/integration/util.go
@@ -111,7 +111,7 @@ func GenerateRequest(logger logr.Logger, prompt, model string, filterMetadata []
 	return req
 }
 
-func GenerateStreamedRequestSet(logger logr.Logger, prompt, model string, filterMetadata []string) []*extProcPb.ProcessingRequest {
+func GenerateStreamedRequestSet(logger logr.Logger, prompt, model, targetModel string, filterMetadata []string) []*extProcPb.ProcessingRequest {
 	requests := []*extProcPb.ProcessingRequest{}
 	headerReq := &extProcPb.ProcessingRequest{
 		Request: &extProcPb.ProcessingRequest_RequestHeaders{
@@ -126,6 +126,10 @@ func GenerateStreamedRequestSet(logger logr.Logger, prompt, model string, filter
 							Key:   metadata.ObjectiveKey,
 							Value: model,
 						},
+						{
+							Key:   metadata.ModelNameRewriteKey,
+							Value: targetModel,
+						},
 					},
 				},
 			},
diff --git a/test/testdata/inferencepool-e2e.yaml b/test/testdata/inferencepool-e2e.yaml
index 2a3c744..7c546ba 100644
--- a/test/testdata/inferencepool-e2e.yaml
+++ b/test/testdata/inferencepool-e2e.yaml
@@ -4,9 +4,11 @@ metadata:
   labels:
   name: vllm-llama3-8b-instruct
 spec:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   selector:
-    app: vllm-llama3-8b-instruct
+    matchLabels:
+      app: vllm-llama3-8b-instruct
   extensionRef:
     name: vllm-llama3-8b-instruct-epp
     namespace: $E2E_NS
diff --git a/test/testdata/inferencepool-leader-election-e2e.yaml b/test/testdata/inferencepool-leader-election-e2e.yaml
new file mode 100644
index 0000000..413fa55
--- /dev/null
+++ b/test/testdata/inferencepool-leader-election-e2e.yaml
@@ -0,0 +1,206 @@
+apiVersion: inference.networking.k8s.io/v1
+kind: InferencePool
+metadata:
+  labels:
+  name: vllm-llama3-8b-instruct
+spec:
+  targetPortNumber: 8000
+  selector:
+    app: vllm-llama3-8b-instruct
+  extensionRef:
+    name: vllm-llama3-8b-instruct-epp
+    namespace: $E2E_NS
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: vllm-llama3-8b-instruct-epp
+  namespace: $E2E_NS
+spec:
+  selector:
+    app: vllm-llama3-8b-instruct-epp
+  ports:
+    - protocol: TCP
+      port: 9002
+      targetPort: 9002
+      appProtocol: http2
+  type: ClusterIP
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: vllm-llama3-8b-instruct-epp
+  namespace: $E2E_NS
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: vllm-llama3-8b-instruct-epp
+  namespace: $E2E_NS
+  labels:
+    app: vllm-llama3-8b-instruct-epp
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: vllm-llama3-8b-instruct-epp
+  template:
+    metadata:
+      labels:
+        app: vllm-llama3-8b-instruct-epp
+    spec:
+      serviceAccountName: vllm-llama3-8b-instruct-epp
+      # Conservatively, this timeout should mirror the longest grace period of the pods within the pool
+      terminationGracePeriodSeconds: 130
+      containers:
+      - name: epp
+        image: $E2E_IMAGE
+        imagePullPolicy: IfNotPresent
+        args:
+        - --pool-name
+        - "vllm-llama3-8b-instruct"
+        - --pool-namespace
+        - "$E2E_NS"
+        - --v
+        - "4"
+        - --zap-encoder
+        - "json"
+        - --grpc-port
+        - "9002"
+        - --grpc-health-port
+        - "9003"
+        - --ha-enable-leader-election
+        - "--config-file"
+        - "/config/default-plugins.yaml"
+        ports:
+        - containerPort: 9002
+        - containerPort: 9003
+        - name: metrics
+          containerPort: 9090
+        livenessProbe:
+          grpc:
+            port: 9003
+            service: liveness
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        readinessProbe:
+          grpc:
+            port: 9003
+            service: readiness
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        volumeMounts:
+        - name: plugins-config-volume
+          mountPath: "/config"
+      volumes:
+      - name: plugins-config-volume
+        configMap:
+          name: plugins-config
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: plugins-config
+  namespace: $E2E_NS
+data:
+  default-plugins.yaml: |
+    apiVersion: inference.networking.x-k8s.io/v1alpha1
+    kind: EndpointPickerConfig
+    plugins:
+    - type: queue-scorer
+    - type: kv-cache-utilization-scorer
+    - type: prefix-cache-scorer
+    schedulingProfiles:
+    - name: default
+      plugins:
+      - pluginRef: queue-scorer
+      - pluginRef: kv-cache-utilization-scorer
+      - pluginRef: prefix-cache-scorer
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: pod-read
+  namespace: $E2E_NS
+rules:
+- apiGroups: [ "inference.networking.x-k8s.io" ]
+  resources: [ "inferenceobjectives", "inferencepools" ]
+  verbs: [ "get", "watch", "list" ]
+- apiGroups: [ "inference.networking.k8s.io" ]
+  resources: [ "inferencepools" ]
+  verbs: [ "get", "watch", "list" ]
+- apiGroups: [ "" ]
+  resources: [ "pods" ]
+  verbs: [ "get", "watch", "list" ]
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: epp-leader-election
+  namespace: $E2E_NS
+rules:
+- apiGroups: [ "coordination.k8s.io" ]
+  resources: [ "leases" ]
+  verbs: [ "get", "list", "watch", "create", "update", "patch", "delete" ]
+- apiGroups: [ "" ]
+  resources: [ "events" ]
+  verbs: [ "create", "patch" ]
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: epp-leader-election-binding
+  namespace: $E2E_NS
+subjects:
+- kind: ServiceAccount
+  name: vllm-llama3-8b-instruct-epp
+  namespace: $E2E_NS
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: epp-leader-election
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: pod-read-binding
+  namespace: $E2E_NS
+subjects:
+- kind: ServiceAccount
+  name: vllm-llama3-8b-instruct-epp
+  namespace: $E2E_NS
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: pod-read
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: auth-reviewer
+rules:
+- apiGroups:
+  - authentication.k8s.io
+  resources:
+  - tokenreviews
+  verbs:
+  - create
+- apiGroups:
+  - authorization.k8s.io
+  resources:
+  - subjectaccessreviews
+  verbs:
+  - create
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: auth-reviewer-binding
+subjects:
+- kind: ServiceAccount
+  name: vllm-llama3-8b-instruct-epp
+  namespace: $E2E_NS
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: auth-reviewer
diff --git a/test/testdata/inferencepool-with-model-hermetic.yaml b/test/testdata/inferencepool-with-model-hermetic.yaml
index 2b41b53..20fea8d 100644
--- a/test/testdata/inferencepool-with-model-hermetic.yaml
+++ b/test/testdata/inferencepool-with-model-hermetic.yaml
@@ -4,9 +4,11 @@ metadata:
   name: vllm-llama3-8b-instruct-pool
   namespace: default
 spec:
-  targetPortNumber: 8000
+  targetPorts:
+    - number: 8000
   selector:
-    app: vllm-llama3-8b-instruct-pool
+    matchLabels:
+      app: vllm-llama3-8b-instruct-pool
   extensionRef:
     name: epp
 ---
@@ -16,7 +18,7 @@ metadata:
   name: sql-lora
   namespace: default
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
     name: vllm-llama3-8b-instruct-pool
   targetModels:
@@ -41,7 +43,7 @@ metadata:
   name: my-model
   namespace: default
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
     name: vllm-llama3-8b-instruct-pool
   targetModels:
@@ -54,6 +56,6 @@ metadata:
   name: direct-model
   namespace: default
 spec:
-  criticality: Critical
+  priority: 2
   poolRef:
-    name: vllm-llama3-8b-instruct-pool
\ No newline at end of file
+    name: vllm-llama3-8b-instruct-pool
diff --git a/test/utils/server.go b/test/utils/server.go
index fd16580..51eb33f 100644
--- a/test/utils/server.go
+++ b/test/utils/server.go
@@ -49,7 +49,7 @@ func PrepareForTestStreamingServer(objectives []*v1alpha2.InferenceObjective, po
 	ctx, cancel := context.WithCancel(context.Background())
 
 	pmc := &metrics.FakePodMetricsClient{}
-	pmf := metrics.NewPodMetricsFactory(pmc, time.Second, time.Second*2)
+	pmf := metrics.NewPodMetricsFactory(pmc, time.Second)
 	ds := datastore.NewDatastore(ctx, pmf)
 
 	initObjs := []client.Object{}
@@ -71,7 +71,7 @@ func PrepareForTestStreamingServer(objectives []*v1alpha2.InferenceObjective, po
 		WithObjects(initObjs...).
 		Build()
 	pool := testutil.MakeInferencePool(poolName).Namespace(namespace).ObjRef()
-	pool.Spec.TargetPortNumber = poolPort
+	pool.Spec.TargetPorts = []v1.Port{{Number: v1.PortNumber(poolPort)}}
 	_ = ds.PoolSet(context.Background(), fakeClient, pool)
 
 	return ctx, cancel, ds, pmc
diff --git a/test/utils/utils.go b/test/utils/utils.go
index c3e2759..8cc19b7 100644
--- a/test/utils/utils.go
+++ b/test/utils/utils.go
@@ -204,6 +204,18 @@ func DeploymentAvailable(ctx context.Context, cli client.Client, deploy *appsv1.
 	gomega.Eventually(checkDeploymentStatus, timeout, interval).WithArguments(ctx, cli, deploy, conditions).Should(gomega.BeTrue())
 }
 
+// DeploymentReadyReplicas checks if the given Deployment has at least `count` ready replicas before the given timeout.
+func DeploymentReadyReplicas(ctx context.Context, cli client.Client, deploy *appsv1.Deployment, count int, timeout, interval time.Duration) {
+	ginkgo.By(fmt.Sprintf("Checking if deployment %s/%s has at least %d ready replica(s)", deploy.Namespace, deploy.Name, count))
+	gomega.Eventually(func(g gomega.Gomega) {
+		var fetchedDeploy appsv1.Deployment
+		err := cli.Get(ctx, types.NamespacedName{Namespace: deploy.Namespace, Name: deploy.Name}, &fetchedDeploy)
+		g.Expect(err).NotTo(gomega.HaveOccurred())
+		g.Expect(fetchedDeploy.Status.ReadyReplicas).To(gomega.BeNumerically(">=", count),
+			fmt.Sprintf("Deployment only has %d ready replicas, want at least %d", fetchedDeploy.Status.ReadyReplicas, count))
+	}, timeout, interval).Should(gomega.Succeed())
+}
+
 // checkDeploymentStatus checks if the given Deployment status matches the expected conditions.
 func checkDeploymentStatus(ctx context.Context, cli client.Client, deploy *appsv1.Deployment, conditions []appsv1.DeploymentCondition) (bool, error) {
 	var fetchedDeploy appsv1.Deployment
diff --git a/test/utils/wrappers.go b/test/utils/wrappers.go
index 1af90b0..f1c0098 100644
--- a/test/utils/wrappers.go
+++ b/test/utils/wrappers.go
@@ -44,9 +44,9 @@ func MakeModelWrapper(namespacedName types.NamespacedName) *InferenceObjectiveWr
 	}
 }
 
-// SetCriticality sets the value of the InferenceObjective.spec.criticality.
-func (m *InferenceObjectiveWrapper) SetCriticality(level v1alpha2.Criticality) *InferenceObjectiveWrapper {
-	m.Spec.Criticality = &level
+// SetPriority sets the value of the InferenceObjective.spec.priority.
+func (m *InferenceObjectiveWrapper) SetPriority(level int) *InferenceObjectiveWrapper {
+	m.Spec.Priority = &level
 	return m
 }
 
@@ -62,12 +62,6 @@ func (m *InferenceObjectiveWrapper) SetPoolRef(name string) *InferenceObjectiveW
 	return m
 }
 
-// SetTargetModels sets the value of the InferenceObjective.spec.targetModels.
-func (m *InferenceObjectiveWrapper) SetTargetModels(models []v1alpha2.TargetModel) *InferenceObjectiveWrapper {
-	m.Spec.TargetModels = models
-	return m
-}
-
 // Obj returns the inner InferenceObjective.
 func (m *InferenceObjectiveWrapper) Obj() *v1alpha2.InferenceObjective {
 	return &m.InferenceObjective