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

Issue:
Docs: How to run integration tests locally in debug mode
## Description
Create a document detailing how to debug integration tests in a local environment.

Discussion:
@zhengkezhou1: /assign

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/.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/config/v1alpha1/defaults.go b/apix/config/v1alpha1/defaults.go
deleted file mode 100644
index b6c50ff..0000000
--- a/apix/config/v1alpha1/defaults.go
+++ /dev/null
@@ -1,47 +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 v1alpha1
-
-// SetDefaults_EndpointPickerConfig sets default values in a
-// EndpointPickerConfig struct.
-//
-// This naming convension is required by the defalter-gen code.
-func SetDefaults_EndpointPickerConfig(cfg *EndpointPickerConfig) {
-	// If no name was given for the plugin, use it's type as the name
-	for idx, pluginConfig := range cfg.Plugins {
-		if pluginConfig.Name == "" {
-			cfg.Plugins[idx].Name = pluginConfig.Type
-		}
-	}
-
-	// If No SchedulerProfiles were specified in the confguration,
-	// create one named default with references to all of the
-	// plugins mentioned in the Plugins section of the configuration.
-	if len(cfg.SchedulingProfiles) == 0 {
-		cfg.SchedulingProfiles = make([]SchedulingProfile, 1)
-
-		thePlugins := []SchedulingPlugin{}
-		for _, pluginConfig := range cfg.Plugins {
-			thePlugins = append(thePlugins, SchedulingPlugin{PluginRef: pluginConfig.Name})
-		}
-
-		cfg.SchedulingProfiles[0] = SchedulingProfile{
-			Name:    "default",
-			Plugins: thePlugins,
-		}
-	}
-}
diff --git a/apix/config/v1alpha1/endpointpickerconfig_types.go b/apix/config/v1alpha1/endpointpickerconfig_types.go
index 92baa6c..705e372 100644
--- a/apix/config/v1alpha1/endpointpickerconfig_types.go
+++ b/apix/config/v1alpha1/endpointpickerconfig_types.go
@@ -18,11 +18,11 @@ package v1alpha1
 
 import (
 	"encoding/json"
+	"fmt"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// +k8s:defaulter-gen=true
 // +kubebuilder:object:root=true
 
 // EndpointPickerConfig is the Schema for the endpointpickerconfigs API
@@ -41,6 +41,14 @@ type EndpointPickerConfig struct {
 	SchedulingProfiles []SchedulingProfile `json:"schedulingProfiles"`
 }
 
+func (cfg EndpointPickerConfig) String() string {
+	return fmt.Sprintf(
+		"{Plugins: %v, SchedulingProfiles: %v}",
+		cfg.Plugins,
+		cfg.SchedulingProfiles,
+	)
+}
+
 // PluginSpec contains the information that describes a plugin that
 // will be instantiated.
 type PluginSpec struct {
@@ -61,6 +69,14 @@ type PluginSpec struct {
 	Parameters json.RawMessage `json:"parameters"`
 }
 
+func (ps PluginSpec) String() string {
+	var parameters string
+	if ps.Parameters != nil {
+		parameters = fmt.Sprintf(", Parameters: %s", ps.Parameters)
+	}
+	return fmt.Sprintf("{%s/%s%s}", ps.Name, ps.Type, parameters)
+}
+
 // SchedulingProfile contains the information to create a SchedulingProfile
 // entry to be used by the scheduler.
 type SchedulingProfile struct {
@@ -75,6 +91,10 @@ type SchedulingProfile struct {
 	Plugins []SchedulingPlugin `json:"plugins"`
 }
 
+func (sp SchedulingProfile) String() string {
+	return fmt.Sprintf("{Name: %s, Plugins: %v}", sp.Name, sp.Plugins)
+}
+
 // SchedulingPlugin describes a plugin that will be associated with a
 // SchedulingProfile entry.
 type SchedulingPlugin struct {
@@ -90,3 +110,11 @@ type SchedulingPlugin struct {
 	// Weight is the weight fo be used if this plugin is a Scorer.
 	Weight *int `json:"weight"`
 }
+
+func (sp SchedulingPlugin) String() string {
+	var weight string
+	if sp.Weight != nil {
+		weight = fmt.Sprintf(", Weight: %d", *sp.Weight)
+	}
+	return fmt.Sprintf("{PluginRef: %s%s}", sp.PluginRef, weight)
+}
diff --git a/apix/config/v1alpha1/zz_generated.defaults.go b/apix/config/v1alpha1/zz_generated.defaults.go
deleted file mode 100644
index 1cf0c42..0000000
--- a/apix/config/v1alpha1/zz_generated.defaults.go
+++ /dev/null
@@ -1,38 +0,0 @@
-//go:build !ignore_autogenerated
-// +build !ignore_autogenerated
-
-/*
-Copyright The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Code generated by defaulter-gen. DO NOT EDIT.
-
-package v1alpha1
-
-import (
-	runtime "k8s.io/apimachinery/pkg/runtime"
-)
-
-// RegisterDefaults adds defaulters functions to the given scheme.
-// Public to allow building arbitrary schemes.
-// All generated defaulters are covering - they call all nested defaulters.
-func RegisterDefaults(scheme *runtime.Scheme) error {
-	scheme.AddTypeDefaultingFunc(&EndpointPickerConfig{}, func(obj interface{}) { SetObjectDefaults_EndpointPickerConfig(obj.(*EndpointPickerConfig)) })
-	return nil
-}
-
-func SetObjectDefaults_EndpointPickerConfig(in *EndpointPickerConfig) {
-	SetDefaults_EndpointPickerConfig(in)
-}
diff --git a/apix/v1alpha2/inferenceobjective_types.go b/apix/v1alpha2/inferenceobjective_types.go
index a2d1f91..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 {
@@ -63,40 +63,21 @@ type InferenceObjectiveList struct {
 // creation timestamp, will be selected to remain valid. In the event of a race
 // condition, one will be selected at random.
 type InferenceObjectiveSpec struct {
-	// ModelName is the name of the model as it will be set in the "model" parameter for an incoming request.
-	// ModelNames must be unique for a referencing InferencePool
-	// (names can be reused for a different pool in the same cluster).
-	// The modelName with the oldest creation timestamp is retained, and the incoming
-	// InferenceObjective's Ready status is set to false with a corresponding reason.
-	// In the rare case of a race condition, one Model will be selected randomly to be considered valid, and the other rejected.
-	// Names can be reserved without an underlying model configured in the pool.
-	// This can be done by specifying a target model and setting the weight to zero,
-	// an error will be returned specifying that no valid target model is found.
-	//
-	// +kubebuilder:validation:MaxLength=256
-	// +kubebuilder:validation:Required
-	// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="modelName is immutable"
-	ModelName string `json:"modelName"`
-
-	// 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.
-	// +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.
-	//
+	// 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
-	// +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.
 	//
@@ -110,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".
@@ -125,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 1007969..6fbcf33 100644
--- a/client-go/applyconfiguration/apix/v1alpha2/inferenceobjectivespec.go
+++ b/client-go/applyconfiguration/apix/v1alpha2/inferenceobjectivespec.go
@@ -18,17 +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 {
-	ModelName    *string                                `json:"modelName,omitempty"`
-	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
@@ -37,32 +31,11 @@ func InferenceObjectiveSpec() *InferenceObjectiveSpecApplyConfiguration {
 	return &InferenceObjectiveSpecApplyConfiguration{}
 }
 
-// WithModelName sets the ModelName 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 ModelName field is set to the value of the last call.
-func (b *InferenceObjectiveSpecApplyConfiguration) WithModelName(value string) *InferenceObjectiveSpecApplyConfiguration {
-	b.ModelName = &value
-	return b
-}
-
-// 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 22140ac..6b14d53 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"
@@ -109,20 +110,6 @@ var (
 		"The path to the certificate for secure serving. The certificate and private key files "+
 			"are assumed to be named tls.crt and tls.key, respectively. If not set, and secureServing is enabled, "+
 			"then a self-signed certificate is used.")
-	// header/metadata flags
-	destinationEndpointHintKey = flag.String(
-		"destination-endpoint-hint-key",
-		runserver.DefaultDestinationEndpointHintKey,
-		"Header and response metadata key used by Envoy to route to the appropriate pod. This must match Envoy configuration.")
-	destinationEndpointHintMetadataNamespace = flag.String(
-		"destination-endpoint-hint-metadata-namespace",
-		runserver.DefaultDestinationEndpointHintMetadataNamespace,
-		"The key for the outer namespace struct in the metadata field of the extproc response that is used to wrap the"+
-			"target endpoint. If not set, then an outer namespace struct should not be created.")
-	fairnessIDHeaderKey = flag.String(
-		"fairness-id-header-key",
-		runserver.DefaultFairnessIDHeaderKey,
-		"The header key used to pass the fairness ID to be used in Flow Control.")
 	// metric flags
 	totalQueuedRequestsMetric = flag.String(
 		"total-queued-requests-metric",
@@ -161,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")
 )
@@ -201,12 +192,12 @@ func bindEnvToFlags() {
 		"MODEL_SERVER_METRICS_PATH":                       "model-server-metrics-path",
 		"MODEL_SERVER_METRICS_SCHEME":                     "model-server-metrics-scheme",
 		"MODEL_SERVER_METRICS_HTTPS_INSECURE_SKIP_VERIFY": "model-server-metrics-https-insecure-skip-verify",
-		"DESTINATION_ENDPOINT_HINT_KEY":                   "destination-endpoint-hint-key",
 		"POOL_NAME":                                       "pool-name",
 		"POOL_NAMESPACE":                                  "pool-namespace",
 		// 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
@@ -284,7 +275,7 @@ func (r *Runner) Run(ctx context.Context) error {
 		ModelServerMetricsScheme: *modelServerMetricsScheme,
 		Client:                   metricsHttpClient,
 	},
-		*refreshMetricsInterval, *metricsStalenessThreshold)
+		*refreshMetricsInterval)
 
 	datastore := datastore.NewDatastore(ctx, pmf)
 
@@ -314,12 +305,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)
@@ -352,20 +359,17 @@ func (r *Runner) Run(ctx context.Context) error {
 
 	// --- Setup ExtProc Server Runner ---
 	serverRunner := &runserver.ExtProcServerRunner{
-		GrpcPort:                                 *grpcPort,
-		DestinationEndpointHintMetadataNamespace: *destinationEndpointHintMetadataNamespace,
-		DestinationEndpointHintKey:               *destinationEndpointHintKey,
-		FairnessIDHeaderKey:                      *fairnessIDHeaderKey,
-		PoolNamespacedName:                       poolNamespacedName,
-		PoolGKNN:                                 poolGKNN,
-		Datastore:                                datastore,
-		SecureServing:                            *secureServing,
-		HealthChecking:                           *healthChecking,
-		CertPath:                                 *certPath,
-		RefreshPrometheusMetricsInterval:         *refreshPrometheusMetricsInterval,
-		MetricsStalenessThreshold:                *metricsStalenessThreshold,
-		Director:                                 director,
-		SaturationDetector:                       saturationDetector,
+		GrpcPort:                         *grpcPort,
+		PoolNamespacedName:               poolNamespacedName,
+		PoolGKNN:                         poolGKNN,
+		Datastore:                        datastore,
+		SecureServing:                    *secureServing,
+		HealthChecking:                   *healthChecking,
+		CertPath:                         *certPath,
+		RefreshPrometheusMetricsInterval: *refreshPrometheusMetricsInterval,
+		MetricsStalenessThreshold:        *metricsStalenessThreshold,
+		Director:                         director,
+		SaturationDetector:               saturationDetector,
 	}
 	if err := serverRunner.SetupWithManager(ctx, mgr); err != nil {
 		setupLog.Error(err, "Failed to setup EPP controllers")
@@ -374,7 +378,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
 	}
 
@@ -412,6 +416,8 @@ func (r *Runner) parsePluginsConfiguration(ctx context.Context) error {
 		return nil // configuring through code, not through file
 	}
 
+	logger := log.FromContext(ctx)
+
 	var configBytes []byte
 	if *configText != "" {
 		configBytes = []byte(*configText)
@@ -425,20 +431,17 @@ func (r *Runner) parsePluginsConfiguration(ctx context.Context) error {
 
 	r.registerInTreePlugins()
 	handle := plugins.NewEppHandle(ctx)
-	config, err := loader.LoadConfig(configBytes, handle)
+	config, err := loader.LoadConfig(configBytes, handle, logger)
 	if err != nil {
 		return fmt.Errorf("failed to load the configuration - %w", err)
 	}
 
-	r.schedulerConfig, err = loader.LoadSchedulerConfig(config.SchedulingProfiles, handle)
-	if err != nil {
-		return fmt.Errorf("failed to create Scheduler configuration - %w", err)
-	}
+	r.schedulerConfig = config.SchedulerConfig
 
 	// Add requestControl plugins
 	r.requestControlConfig.AddPlugins(handle.GetAllPlugins()...)
 
-	log.FromContext(ctx).Info("loaded configuration from file/text successfully")
+	logger.Info("loaded configuration from file/text successfully")
 	return nil
 }
 
@@ -471,11 +474,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-config.yaml b/config/charts/inferencepool/templates/epp-config.yaml
index 26147c2..e6e85d8 100644
--- a/config/charts/inferencepool/templates/epp-config.yaml
+++ b/config/charts/inferencepool/templates/epp-config.yaml
@@ -11,8 +11,6 @@ data:
     - type: queue-scorer
     - type: kv-cache-utilization-scorer
     - type: prefix-cache-scorer
-    - type: max-score-picker
-    - type: single-profile-handler
     schedulingProfiles:
     - name: default
       plugins:
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 df8d947..5f0c2fd 100644
--- a/config/charts/inferencepool/values.yaml
+++ b/config/charts/inferencepool/values.yaml
@@ -21,15 +21,10 @@ inferenceExtension:
   #     - type: custom-scorer
   #       parameters:
   #         custom-threshold: 64
-  #     - type: max-score-picker
-  #     - type: single-profile-handler
   #     schedulingProfiles:
   #     - name: default
   #       plugins:
   #       - pluginRef: custom-scorer
-  #         weight: 1
-  #       - pluginRef: max-score-picker
-  #         weight: 1
 
   # Example environment variables:
   # env:
@@ -39,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 d69803e..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,43 +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
-              modelName:
-                description: |-
-                  ModelName is the name of the model as it will be set in the "model" parameter for an incoming request.
-                  ModelNames must be unique for a referencing InferencePool
-                  (names can be reused for a different pool in the same cluster).
-                  The modelName with the oldest creation timestamp is retained, and the incoming
-                  InferenceObjective's Ready status is set to false with a corresponding reason.
-                  In the rare case of a race condition, one Model will be selected randomly to be considered valid, and the other rejected.
-                  Names can be reserved without an underlying model configured in the pool.
-                  This can be done by specifying a target model and setting the weight to zero,
-                  an error will be returned specifying that no valid target model is found.
-                maxLength: 256
-                type: string
-                x-kubernetes-validations:
-                - message: modelName is immutable
-                  rule: self == oldSelf
               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])?)*$
@@ -124,54 +93,22 @@ 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.
-
-                        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.
+                  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.
 
-                        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:
-            - modelName
             - poolRef
             type: object
           status:
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 b8ceb22..3b7ba91 100644
--- a/config/manifests/inferenceobjective.yaml
+++ b/config/manifests/inferenceobjective.yaml
@@ -3,22 +3,19 @@ kind: InferenceObjective
 metadata:
   name: food-review
 spec:
-  modelName: food-review
-  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:
-  modelName: meta-llama/Llama-3.1-8B-Instruct
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
 ---
 apiVersion: inference.networking.x-k8s.io/v1alpha2
@@ -26,7 +23,7 @@ kind: InferenceObjective
 metadata:
   name: base-model-cpu
 spec:
-  modelName: Qwen/Qwen2.5-1.5B-Instruct
-  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 a1eb3e0..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
 ---
@@ -112,8 +114,6 @@ data:
     - type: queue-scorer
     - type: kv-cache-utilization-scorer
     - type: prefix-cache-scorer
-    - type: max-score-picker
-    - type: single-profile-handler
     schedulingProfiles:
     - name: default
       plugins:
diff --git a/config/manifests/regression-testing/inferenceobjective.yaml b/config/manifests/regression-testing/inferenceobjective.yaml
index 586cec3..eede557 100644
--- a/config/manifests/regression-testing/inferenceobjective.yaml
+++ b/config/manifests/regression-testing/inferenceobjective.yaml
@@ -3,13 +3,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-0
 spec:
-  modelName: adapter-0
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-0
-    weight: 100
 
 ---
 
@@ -18,13 +15,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-1
 spec:
-  modelName: adapter-1
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-1
-    weight: 100
 
 ---
 
@@ -33,13 +27,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-2
 spec:
-  modelName: adapter-2
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-2
-    weight: 100
 
 ---
 
@@ -48,13 +39,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-3
 spec:
-  modelName: adapter-3
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-3
-    weight: 100
 
 ---
 
@@ -63,13 +51,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-4
 spec:
-  modelName: adapter-4
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-4
-    weight: 100
 
 ---
 
@@ -78,13 +63,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-5
 spec:
-  modelName: adapter-5
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-5
-    weight: 100
 
 ---
 
@@ -93,13 +75,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-6
 spec:
-  modelName: adapter-6
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-6
-    weight: 100
 
 ---
 
@@ -108,13 +87,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-7
 spec:
-  modelName: adapter-7
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-7
-    weight: 100
 
 ---
 
@@ -123,13 +99,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-8
 spec:
-  modelName: adapter-8
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-8
-    weight: 100
 
 ---
 
@@ -138,13 +111,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-9
 spec:
-  modelName: adapter-9
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-9
-    weight: 100
 
 ---
 
@@ -153,13 +123,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-10
 spec:
-  modelName: adapter-10
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-10
-    weight: 100
 
 ---
 
@@ -168,13 +135,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-11
 spec:
-  modelName: adapter-11
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-11
-    weight: 100
 
 ---
 
@@ -183,13 +147,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-12
 spec:
-  modelName: adapter-12
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-12
-    weight: 100
 
 
 ---
@@ -199,13 +160,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-13
 spec:
-  modelName: adapter-13
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-13
-    weight: 100
 
 
 ---
@@ -215,13 +173,10 @@ kind: InferenceObjective
 metadata:
   name: adapter-14
 spec:
-  modelName: adapter-14
-  criticality: Critical
+  priority: 2
   poolRef:
+    group: inference.networking.k8s.io
     name: vllm-llama3-8b-instruct
-  targetModels:
-  - name: adapter-14
-    weight: 100
 
 ---
 
@@ -231,7 +186,7 @@ kind: InferenceObjective
 metadata:
   name: base-model
 spec:
-  modelName: meta-llama/Llama-3.1-8B-Instruct
-  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/reports/v0.5.1/gateway/kgateway/README.md b/conformance/reports/v0.5.1/gateway/kgateway/README.md
new file mode 100644
index 0000000..d680d7f
--- /dev/null
+++ b/conformance/reports/v0.5.1/gateway/kgateway/README.md
@@ -0,0 +1,75 @@
+# Kgateway
+
+## Table of Contents
+
+| Extension Version Tested | Profile Tested | Implementation Version | Mode    | Report                                                                     |
+|--------------------------|----------------|------------------------|---------|----------------------------------------------------------------------------|
+| v0.5.1                   | Gateway        | v2.0.4                 | default | [v2.0.4 report](./inference-v2.0.4-report.yaml)   |
+
+## Reproduce
+
+This is a mirror of the kgateway [inference conformance GHA workflow](https://github.com/kgateway-dev/kgateway/blob/v2.0.x/.github/actions/kube-inference-extension-conformance-tests/action.yaml).
+
+### 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), [git](https://git-scm.com/downloads), and [make](https://www.gnu.org/software/make/) command-line tools installed.
+
+### Steps
+
+1. Set the environment variables use by the proceeding steps:
+
+   ```sh
+   # The kgateway version
+   export VERSION=v2.0.4
+   # Skip building and loading the kgateway images
+   export SKIP_DOCKER=true
+   # Install Gateway API and Inference Extension CRDs
+   export CONFORMANCE=true
+   ```
+
+2. Clone the kgateway repository and checkout the release:
+
+   ```sh
+   git clone -b $VERSION https://github.com/kgateway-dev/kgateway.git && cd kgateway
+   ```
+
+3. Create a KinD cluster:
+
+   ```sh
+   make kind-setup
+   ```
+
+4. Install the kgateway CRDs:
+
+   ```sh
+   helm upgrade -i --create-namespace --namespace kgateway-system \
+   --version $VERSION kgateway-crds oci://cr.kgateway.dev/kgateway-dev/charts/kgateway-crds
+   ```
+
+5. Install kgateway with Inference Extension enabled:
+
+   ```sh
+   helm upgrade -i --namespace kgateway-system --version $VERSION \
+   kgateway oci://cr.kgateway.dev/kgateway-dev/charts/kgateway --set inferenceExtension.enabled=true
+   ```
+
+6. Wait for the kgateway rollout to complete:
+
+   ```sh
+   kubectl rollout status deploy/kgateway -n kgateway-system
+   ```
+
+7. Run the conformance tests:
+
+   ```sh
+   make gie-conformance
+   ```
+
+8. View and verify the conformance report:
+
+   ```sh
+   cat _test/conformance/inference-$VERSION-report.yaml
+   ```
diff --git a/conformance/reports/v0.5.1/gateway/kgateway/inference-v2.0.4-report.yaml b/conformance/reports/v0.5.1/gateway/kgateway/inference-v2.0.4-report.yaml
new file mode 100644
index 0000000..55943a4
--- /dev/null
+++ b/conformance/reports/v0.5.1/gateway/kgateway/inference-v2.0.4-report.yaml
@@ -0,0 +1,23 @@
+GatewayAPIInferenceExtensionVersion: v0.5.1
+apiVersion: gateway.networking.k8s.io/v1
+date: "2025-08-06T15:05:42-07:00"
+gatewayAPIChannel: experimental
+gatewayAPIVersion: v1.3.0
+implementation:
+  contact:
+  - github.com/kgateway-dev/kgateway/issues/new/choose
+  organization: kgateway-dev
+  project: kgateway
+  url: github.com/kgateway-dev/kgateway
+  version: v2.0.4
+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 dda72fa..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
@@ -343,15 +347,10 @@ data:
     kind: EndpointPickerConfig
     plugins:
     - type: header-based-testing-filter
-    - type: max-score-picker
-      parameters:
-        maxNumOfEndpoints: 1
-    - type: single-profile-handler
     schedulingProfiles:
     - name: conformance-profile
       plugins:
       - pluginRef: header-based-testing-filter
-      - pluginRef: max-score-picker
 ---
 # --- Required Role and RoleBinding for Conformance Test for EPP ---
 apiVersion: rbac.authorization.k8s.io/v1
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/002-api-proposal/README.md b/docs/proposals/002-api-proposal/README.md
index e82d469..b8bf14b 100644
--- a/docs/proposals/002-api-proposal/README.md
+++ b/docs/proposals/002-api-proposal/README.md
@@ -341,7 +341,6 @@ kind: InferenceModel
 metadata:
   name: sql-code-assist
 spec:
-  modelName: sql-code-assist
   poolRef:
     name: base-model-pool
 ---
@@ -350,7 +349,6 @@ kind: InferenceModel
 metadata:
   name: npc-bot
 spec:
-  modelName: npc-bot
   criticality: Critical
   targetModels:
     - name: npc-bot-v1
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 dfa47fd..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
@@ -94,7 +94,7 @@ type InferenceObjectivesSpec struct {
   PoolRef InferenceObjectReference
 
   // this is a departure from InferenceModel that used string for criticality.
-	// We got quite a bit of feedback around allowing for custom criticality bands, so an int/enum is more flexible & carries inherent stack rank value.
+  // We got quite a bit of feedback around allowing for custom criticality bands, so an int/enum is more flexible & carries inherent stack rank value.
   Criticality *int
 }
 
@@ -135,7 +135,7 @@ type InferenceObjectivesSpec struct {
   PoolRef InferenceObjectReference
 
   // this is a departure from InferenceModel that used string for criticality.
-	// We got quite a bit of feedback around allowing for custom criticality bands, so an int/enum is more flexible & carries inherent stack rank value.
+  // We got quite a bit of feedback around allowing for custom criticality bands, so an int/enum is more flexible & carries inherent stack rank value.
   Criticality *int
   PerformanceObjectives NotYetDefinedSLOCRD
   PerformanceObjectivesRef *InferenceObjectReference
diff --git a/go.mod b/go.mod
index 56fce1f..619d8df 100644
--- a/go.mod
+++ b/go.mod
@@ -15,12 +15,13 @@ require (
 	github.com/prometheus/client_golang v1.23.0
 	github.com/prometheus/client_model v0.6.2
 	github.com/prometheus/common v0.65.0
+	github.com/prometheus/prometheus v0.305.0
 	github.com/stretchr/testify v1.10.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/sync v0.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
@@ -43,14 +44,15 @@ require (
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/blang/semver/v4 v4.0.0 // indirect
-	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/dennwc/varint v1.0.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
@@ -64,9 +66,10 @@ require (
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/cel-go v0.23.2 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
-	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
+	github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // indirect
 	github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
+	github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
 	github.com/huandu/xstrings v1.3.3 // indirect
 	github.com/imdario/mergo v0.3.16 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -90,30 +93,31 @@ require (
 	github.com/stoewer/go-strcase v1.3.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
 	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
 	go.opentelemetry.io/otel/metric v1.36.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
 	go.opentelemetry.io/otel/trace v1.36.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.4.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/crypto v0.40.0 // indirect
-	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/mod v0.26.0 // indirect
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/oauth2 v0.30.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/term v0.33.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	golang.org/x/time v0.9.0 // indirect
+	golang.org/x/time v0.12.0 // indirect
 	golang.org/x/tools v0.35.0 // indirect
 	golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 9a0d8a4..9ea103f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,21 +1,65 @@
 cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
 cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
+cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
+cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
+cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
+github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
+github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
+github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
+github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
@@ -25,6 +69,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
+github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
 github.com/elastic/crd-ref-docs v0.2.0 h1:U17MyGX71j4qfKTvYxbR4qZGoA1hc2thy7kseGYmP+o=
 github.com/elastic/crd-ref-docs v0.2.0/go.mod h1:0bklkJhTG7nC6AVsdDi0wt5bGoqvzdZSzMMQkilZ6XM=
 github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
@@ -39,8 +85,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
 github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -64,8 +110,12 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
 github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4=
@@ -78,14 +128,22 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
-github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18=
+github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
+github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
+github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
+github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
@@ -96,6 +154,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -123,16 +183,23 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
+github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
 github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
 github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY=
 github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@@ -150,6 +217,10 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/prometheus/prometheus v0.305.0 h1:UO/LsM32/E9yBDtvQj8tN+WwhbyWKR10lO35vmFLx0U=
+github.com/prometheus/prometheus v0.305.0/go.mod h1:JG+jKIDUJ9Bn97anZiCjwCxRyAx+lpcEQ0QnZlUlbwY=
+github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk=
+github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE=
 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -176,14 +247,14 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
 go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
 go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
 go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
 go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
 go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
@@ -192,8 +263,10 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
 go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
 go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
 go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
-go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
-go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -211,8 +284,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
@@ -241,8 +314,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
 golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
-golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
-golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -259,14 +332,16 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
 gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs=
+google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+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/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/config/config.go b/pkg/epp/config/config.go
new file mode 100644
index 0000000..7b07a07
--- /dev/null
+++ b/pkg/epp/config/config.go
@@ -0,0 +1,24 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package config
+
+import "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
+
+// Config is the configuration loaded from the text based configuration
+type Config struct {
+	SchedulerConfig *scheduling.SchedulerConfig
+}
diff --git a/pkg/epp/config/loader/configloader.go b/pkg/epp/config/loader/configloader.go
index 5e78a84..8e80b03 100644
--- a/pkg/epp/config/loader/configloader.go
+++ b/pkg/epp/config/loader/configloader.go
@@ -20,83 +20,83 @@ import (
 	"errors"
 	"fmt"
 
+	"github.com/go-logr/logr"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apimachinery/pkg/util/sets"
 
 	configapi "sigs.k8s.io/gateway-api-inference-extension/apix/config/v1alpha1"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/config"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
-)
-
-const (
-	// DefaultScorerWeight is the weight used for scorers referenced in the
-	// configuration without explicit weights.
-	DefaultScorerWeight = 1
 )
 
 var scheme = runtime.NewScheme()
 
 func init() {
-	configapi.SchemeBuilder.Register(configapi.RegisterDefaults)
 	utilruntime.Must(configapi.Install(scheme))
 }
 
 // Load config from supplied text that was converted to []byte
-func LoadConfig(configBytes []byte, handle plugins.Handle) (*configapi.EndpointPickerConfig, error) {
-	config := &configapi.EndpointPickerConfig{}
-
-	codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict)
-	err := runtime.DecodeInto(codecs.UniversalDecoder(), configBytes, config)
+func LoadConfig(configBytes []byte, handle plugins.Handle, logger logr.Logger) (*config.Config, error) {
+	rawConfig, err := loadRawConfig(configBytes)
 	if err != nil {
-		return nil, fmt.Errorf("the configuration is invalid - %w", err)
+		return nil, err
 	}
 
+	logger.Info("Loaded configuration", "config", rawConfig)
+
+	setDefaultsPhaseOne(rawConfig)
+
 	// instantiate loaded plugins
-	if err = instantiatePlugins(config.Plugins, handle); err != nil {
+	if err = instantiatePlugins(rawConfig.Plugins, handle); err != nil {
 		return nil, fmt.Errorf("failed to instantiate plugins - %w", err)
 	}
 
-	if err = validateSchedulingProfiles(config); err != nil {
+	setDefaultsPhaseTwo(rawConfig, handle)
+
+	logger.Info("Configuration with defaults set", "config", rawConfig)
+
+	if err = validateSchedulingProfiles(rawConfig); err != nil {
 		return nil, fmt.Errorf("failed to validate scheduling profiles - %w", err)
 	}
 
+	config := &config.Config{}
+
+	config.SchedulerConfig, err = loadSchedulerConfig(rawConfig.SchedulingProfiles, handle)
+	if err != nil {
+		return nil, err
+	}
+
 	return config, nil
 }
 
-func LoadSchedulerConfig(configProfiles []configapi.SchedulingProfile, handle plugins.Handle) (*scheduling.SchedulerConfig, error) {
+func loadRawConfig(configBytes []byte) (*configapi.EndpointPickerConfig, error) {
+	rawConfig := &configapi.EndpointPickerConfig{}
+
+	codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict)
+	err := runtime.DecodeInto(codecs.UniversalDecoder(), configBytes, rawConfig)
+	if err != nil {
+		return nil, fmt.Errorf("the configuration is invalid - %w", err)
+	}
+	return rawConfig, nil
+}
+
+func loadSchedulerConfig(configProfiles []configapi.SchedulingProfile, handle plugins.Handle) (*scheduling.SchedulerConfig, error) {
 	profiles := map[string]*framework.SchedulerProfile{}
 	for _, namedProfile := range configProfiles {
 		profile := framework.NewSchedulerProfile()
-		pickerAdded := false
 		for _, plugin := range namedProfile.Plugins {
 			referencedPlugin := handle.Plugin(plugin.PluginRef)
 			if scorer, ok := referencedPlugin.(framework.Scorer); ok {
-				// Set default weight if one wasn't set in the configuration
-				weight := DefaultScorerWeight
-				if plugin.Weight != nil {
-					weight = *plugin.Weight
-				}
-				referencedPlugin = framework.NewWeightedScorer(scorer, weight)
-			}
-			if _, ok := referencedPlugin.(framework.Picker); ok {
-				pickerAdded = true
+				referencedPlugin = framework.NewWeightedScorer(scorer, *plugin.Weight)
 			}
 			if err := profile.AddPlugins(referencedPlugin); err != nil {
 				return nil, fmt.Errorf("failed to load scheduler config - %w", err)
 			}
 		}
-		if !pickerAdded {
-			// There isn't a picker in this profile, add one
-			thePicker := picker.NewMaxScorePicker(picker.DefaultMaxNumOfEndpoints)
-			if err := profile.AddPlugins(thePicker); err != nil {
-				return nil, fmt.Errorf("failed to load scheduler config - %w", err)
-			}
-		}
 		profiles[namedProfile.Name] = profile
 	}
 
@@ -110,10 +110,7 @@ func LoadSchedulerConfig(configProfiles []configapi.SchedulingProfile, handle pl
 		}
 	}
 	if profileHandler == nil {
-		if len(profiles) != 1 {
-			return nil, errors.New("no profile handler was specified")
-		}
-		profileHandler = profile.NewSingleProfileHandler()
+		return nil, errors.New("no profile handler was specified")
 	}
 
 	return scheduling.NewSchedulerConfig(profileHandler, profiles), nil
@@ -160,9 +157,6 @@ func validateSchedulingProfiles(config *configapi.EndpointPickerConfig) error {
 		}
 		profileNames.Insert(profile.Name)
 
-		if len(profile.Plugins) == 0 {
-			return fmt.Errorf("SchedulingProfile '%s' must have at least one plugin", profile.Name)
-		}
 		for _, plugin := range profile.Plugins {
 			if len(plugin.PluginRef) == 0 {
 				return fmt.Errorf("SchedulingProfile '%s' plugins must have a plugin reference", profile.Name)
diff --git a/pkg/epp/config/loader/defaults.go b/pkg/epp/config/loader/defaults.go
new file mode 100644
index 0000000..7be0d98
--- /dev/null
+++ b/pkg/epp/config/loader/defaults.go
@@ -0,0 +1,133 @@
+/*
+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 loader
+
+import (
+	configapi "sigs.k8s.io/gateway-api-inference-extension/apix/config/v1alpha1"
+	"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/picker"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
+)
+
+const (
+	// DefaultScorerWeight is the weight used for scorers referenced in the
+	// configuration without explicit weights.
+	DefaultScorerWeight = 1
+)
+
+// The code below sets the defaults in the configuration. It is done in two parts:
+//   1) Before the plugins are instantiated, for those defaults that can be set
+//      without knowing the concrete type of plugins
+//   2) After the plugins are instantiated, for things that one needs to know the
+//      concrete type of the plugins
+
+var defaultScorerWeight = DefaultScorerWeight
+
+// setDefaultsPhaseOne Performs the first phase of setting configuration defaults.
+// In particuylar it:
+//  1. Sets the name of plugins, for which one wasn't specified
+func setDefaultsPhaseOne(cfg *configapi.EndpointPickerConfig) {
+	// If no name was given for the plugin, use it's type as the name
+	for idx, pluginConfig := range cfg.Plugins {
+		if pluginConfig.Name == "" {
+			cfg.Plugins[idx].Name = pluginConfig.Type
+		}
+	}
+}
+
+// setDefaultsPhaseTwo Performs the second phase of setting configuration defaults.
+// In particular it:
+//  1. Adds a default SchedulingProfile if one wasn't specified.
+//  2. Adds an instance of the SingleProfileHandler, if no profile handler was
+//     specified and the configuration has only one SchedulingProfile
+//  3. Sets a default weight for all scorers without a weight
+//  4. Adds a picker (MaxScorePicker) to all SchedulingProfiles that don't have a picker
+func setDefaultsPhaseTwo(cfg *configapi.EndpointPickerConfig, handle plugins.Handle) {
+	allPlugins := handle.GetAllPluginsWithNames()
+
+	// If No SchedulerProfiles were specified in the confguration,
+	// create one named default with references to all of the scheduling
+	// plugins mentioned in the Plugins section of the configuration.
+	if len(cfg.SchedulingProfiles) == 0 {
+		cfg.SchedulingProfiles = make([]configapi.SchedulingProfile, 1)
+
+		thePlugins := []configapi.SchedulingPlugin{}
+		for pluginName, plugin := range allPlugins {
+			switch plugin.(type) {
+			case framework.Filter, framework.Picker, framework.PostCycle, framework.Scorer:
+				thePlugins = append(thePlugins, configapi.SchedulingPlugin{PluginRef: pluginName})
+			}
+		}
+
+		cfg.SchedulingProfiles[0] = configapi.SchedulingProfile{
+			Name:    "default",
+			Plugins: thePlugins,
+		}
+	}
+
+	// Add an instance of the SingleProfileHandler, if no profile handler was
+	// specified and the configuration has only one SchedulingProfile
+	if len(cfg.SchedulingProfiles) == 1 {
+		profileHandlerFound := false
+		for _, plugin := range allPlugins {
+			if _, ok := plugin.(framework.ProfileHandler); ok {
+				profileHandlerFound = true
+				break
+			}
+		}
+		if !profileHandlerFound {
+			handle.AddPlugin(profile.SingleProfileHandlerType, profile.NewSingleProfileHandler())
+			cfg.Plugins = append(cfg.Plugins,
+				configapi.PluginSpec{Name: profile.SingleProfileHandlerType,
+					Type: profile.SingleProfileHandlerType,
+				})
+		}
+	}
+
+	var maxScorePicker string
+	for pluginName, plugin := range allPlugins {
+		if _, ok := plugin.(framework.Picker); ok {
+			maxScorePicker = pluginName
+			break
+		}
+	}
+	if maxScorePicker == "" {
+		handle.AddPlugin(picker.MaxScorePickerType, picker.NewMaxScorePicker(picker.DefaultMaxNumOfEndpoints))
+		maxScorePicker = picker.MaxScorePickerType
+		cfg.Plugins = append(cfg.Plugins, configapi.PluginSpec{Name: maxScorePicker, Type: maxScorePicker})
+	}
+
+	for idx, theProfile := range cfg.SchedulingProfiles {
+		hasPicker := false
+		for pluginIdx, plugin := range theProfile.Plugins {
+			referencedPlugin := handle.Plugin(plugin.PluginRef)
+			if _, ok := referencedPlugin.(framework.Scorer); ok && plugin.Weight == nil {
+				theProfile.Plugins[pluginIdx].Weight = &defaultScorerWeight
+				cfg.SchedulingProfiles[idx] = theProfile
+			} else if _, ok := referencedPlugin.(framework.Picker); ok {
+				hasPicker = true
+				break
+			}
+		}
+		if !hasPicker {
+			theProfile.Plugins =
+				append(theProfile.Plugins, configapi.SchedulingPlugin{PluginRef: maxScorePicker})
+			cfg.SchedulingProfiles[idx] = theProfile
+		}
+	}
+}
diff --git a/pkg/epp/controller/inferenceobjective_reconciler.go b/pkg/epp/controller/inferenceobjective_reconciler.go
index bdeb039..53bce86 100644
--- a/pkg/epp/controller/inferenceobjective_reconciler.go
+++ b/pkg/epp/controller/inferenceobjective_reconciler.go
@@ -18,10 +18,8 @@ package controller
 
 import (
 	"context"
-	"fmt"
 
 	"k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/event"
@@ -41,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")
@@ -56,62 +54,21 @@ 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.
-		err := c.handleObjectiveDeleted(ctx, req.NamespacedName)
-		return ctrl.Result{}, err
+		c.Datastore.ObjectiveDelete(req.NamespacedName)
+		return ctrl.Result{}, nil
 	}
 
 	// Add or update if the InferenceObjective instance has a creation timestamp older than the existing entry of the model.
-	logger = logger.WithValues("poolRef", infObjective.Spec.PoolRef).WithValues("modelName", infObjective.Spec.ModelName)
-	if !c.Datastore.ObjectiveSetIfOlder(infObjective) {
-		logger.Info("Skipping InferenceObjective, existing instance has older creation timestamp")
-	} else {
-		logger.Info("Added/Updated InferenceObjective")
-	}
+	logger = logger.WithValues("poolRef", infObjective.Spec.PoolRef)
+	c.Datastore.ObjectiveSet(infObjective)
+	logger.Info("Added/Updated InferenceObjective")
 
 	return ctrl.Result{}, nil
 }
 
-func (c *InferenceObjectiveReconciler) handleObjectiveDeleted(ctx context.Context, req types.NamespacedName) error {
-	logger := log.FromContext(ctx)
-
-	// We will lookup and delete the modelName associated with this object, and search for
-	// other instances referencing the same modelName if exist, and store the oldest in
-	// its place. This ensures that the InferenceObjective with the oldest creation
-	// timestamp is active.
-	existing := c.Datastore.ObjectiveDelete(req)
-	if existing == nil {
-		// No entry exists in the first place, nothing to do.
-		return nil
-	}
-	logger.Info("InferenceObjective removed from datastore", "poolRef", existing.Spec.PoolRef, "modelName", existing.Spec.ModelName)
-
-	// TODO(#409): replace this backfill logic with one that is based on InferenceObjective Ready conditions once those are set by an external controller.
-	updated, err := c.Datastore.ObjectiveResync(ctx, c.Reader, existing.Spec.ModelName)
-	if err != nil {
-		return err
-	}
-	if updated {
-		logger.Info("Model replaced.", "modelName", existing.Spec.ModelName)
-	}
-	return nil
-}
-
-func indexInferenceObjectivesByModelName(obj client.Object) []string {
-	m, ok := obj.(*v1alpha2.InferenceObjective)
-	if !ok {
-		return nil
-	}
-	return []string{m.Spec.ModelName}
-}
-
 func (c *InferenceObjectiveReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
-	// Create an index on ModelName for InferenceObjective objects.
-	indexer := mgr.GetFieldIndexer()
-	if err := indexer.IndexField(ctx, &v1alpha2.InferenceObjective{}, datastore.ModelNameIndexKey, indexInferenceObjectivesByModelName); err != nil {
-		return fmt.Errorf("setting index on ModelName for InferenceObjective: %w", err)
-	}
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha2.InferenceObjective{}).
 		WithEventFilter(predicate.Funcs{
@@ -126,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/attributemap.go b/pkg/epp/datalayer/attributemap.go
index 45b39a5..614bf57 100644
--- a/pkg/epp/datalayer/attributemap.go
+++ b/pkg/epp/datalayer/attributemap.go
@@ -32,12 +32,11 @@ type AttributeMap interface {
 	Put(string, Cloneable)
 	Get(string) (Cloneable, bool)
 	Keys() []string
-	Clone() *Attributes
 }
 
 // Attributes provides a goroutine-safe implementation of AttributeMap.
 type Attributes struct {
-	data sync.Map
+	data sync.Map // key: attribute name (string), value: attribute value (opaque, Cloneable)
 }
 
 // NewAttributes returns a new instance of Attributes.
@@ -76,7 +75,7 @@ func (a *Attributes) Keys() []string {
 	return keys
 }
 
-// Clone creates a deep copy of the entire Attributes map.
+// Clone creates a deep copy of the entire attribute map.
 func (a *Attributes) Clone() *Attributes {
 	clone := NewAttributes()
 	a.data.Range(func(key, value any) bool {
diff --git a/pkg/epp/datalayer/collector.go b/pkg/epp/datalayer/collector.go
new file mode 100644
index 0000000..87d2a62
--- /dev/null
+++ b/pkg/epp/datalayer/collector.go
@@ -0,0 +1,156 @@
+/*
+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"
+	"errors"
+	"sync"
+	"time"
+
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
+)
+
+// TODO:
+// currently the data store is expected to manage the state of multiple
+// Collectors (e.g., using sync.Map mapping pod to its Collector). Alternatively,
+// this can be encapsulated in this file, providing the data store with an interface
+// to only update on endpoint addition/change and deletion. This can also be used
+// to centrally track statistics such errors, active routines, etc.
+
+const (
+	defaultCollectionTimeout = time.Second
+)
+
+// Ticker implements a time source for periodic invocation.
+// The Ticker is passed in as parameter a Collector to allow control over time
+// progress in tests, ensuring tests are deterministic and fast.
+type Ticker interface {
+	Channel() <-chan time.Time
+	Stop()
+}
+
+// TimeTicker implements a Ticker based on time.Ticker.
+type TimeTicker struct {
+	*time.Ticker
+}
+
+// NewTimeTicker returns a new time.Ticker with the configured duration.
+func NewTimeTicker(d time.Duration) Ticker {
+	return &TimeTicker{
+		Ticker: time.NewTicker(d),
+	}
+}
+
+// Channel exposes the ticker's channel.
+func (t *TimeTicker) Channel() <-chan time.Time {
+	return t.C
+}
+
+// Collector runs the data collection for a single endpoint.
+type Collector struct {
+	// per-endpoint context and cancellation
+	ctx    context.Context
+	cancel context.CancelFunc
+
+	// goroutine management
+	startOnce sync.Once
+	stopOnce  sync.Once
+
+	// TODO: optional metrics tracking collection (e.g., errors, invocations, ...)
+}
+
+// NewCollector returns a new collector.
+func NewCollector() *Collector {
+	return &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())
+			logger.V(logging.DEFAULT).Info("starting collection")
+
+			defer func() {
+				logger.V(logging.DEFAULT).Info("terminating collection")
+				ticker.Stop()
+			}()
+
+			close(ready) // signal ready to accept ticks
+
+			for {
+				select {
+				case <-c.ctx.Done(): // per endpoint context cancelled
+					return
+				case <-ticker.Channel():
+					for _, src := range sources {
+						ctx, cancel := context.WithTimeout(c.ctx, defaultCollectionTimeout)
+						_ = src.Collect(ctx, endpoint) // TODO: track errors per collector?
+						cancel()                       // release the ctx timeout resources
+					}
+				}
+			}
+		}(ep, sources)
+	})
+
+	if !started {
+		return errors.New("collector start called multiple times")
+	}
+
+	// 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.
+func (c *Collector) Stop() error {
+	if c.ctx == nil || c.cancel == nil {
+		return errors.New("collector stop called before start")
+	}
+
+	stopped := false
+	c.stopOnce.Do(func() {
+		stopped = true
+		c.cancel()
+	})
+
+	if !stopped {
+		return errors.New("collector stop called multiple times")
+	}
+	return nil
+}
diff --git a/pkg/epp/datalayer/datasource.go b/pkg/epp/datalayer/datasource.go
index 9539351..6ad7b29 100644
--- a/pkg/epp/datalayer/datasource.go
+++ b/pkg/epp/datalayer/datasource.go
@@ -17,6 +17,7 @@ limitations under the License.
 package datalayer
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"reflect"
@@ -36,7 +37,7 @@ type DataSource interface {
 	// Collect is triggered by the data layer framework to fetch potentially new
 	// data for an endpoint. Collect calls registered Extractors to convert the
 	// raw data into structured attributes.
-	Collect(ep Endpoint)
+	Collect(ctx context.Context, ep Endpoint) error
 }
 
 // Extractor transforms raw data into structured attributes.
@@ -46,7 +47,7 @@ type Extractor interface {
 	ExpectedInputType() reflect.Type
 	// Extract transforms the raw data source output into a concrete structured
 	// attribute, stored on the given endpoint.
-	Extract(data any, ep Endpoint)
+	Extract(ctx context.Context, data any, ep Endpoint) error
 }
 
 var defaultDataSources = DataSourceRegistry{}
diff --git a/pkg/epp/datalayer/endpoint.go b/pkg/epp/datalayer/endpoint.go
index 74c0c0e..2a72886 100644
--- a/pkg/epp/datalayer/endpoint.go
+++ b/pkg/epp/datalayer/endpoint.go
@@ -17,6 +17,9 @@ limitations under the License.
 package datalayer
 
 import (
+	"fmt"
+	"sync/atomic"
+
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -34,7 +37,65 @@ type EndpointMetricsState interface {
 
 // Endpoint represents an inference serving endpoint and its related attributes.
 type Endpoint interface {
+	fmt.Stringer
 	EndpointPodState
 	EndpointMetricsState
 	AttributeMap
 }
+
+// ModelServer is an implementation of the Endpoint interface.
+type ModelServer struct {
+	pod        atomic.Pointer[PodInfo]
+	metrics    atomic.Pointer[Metrics]
+	attributes *Attributes
+}
+
+// NewEndpoint return a new (uninitialized) ModelServer.
+func NewEndpoint() *ModelServer {
+	return &ModelServer{
+		attributes: NewAttributes(),
+	}
+}
+
+// String returns a representation of the ModelServer. For brevity, only names of
+// 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())
+}
+
+func (srv *ModelServer) GetPod() *PodInfo {
+	return srv.pod.Load()
+}
+
+func (srv *ModelServer) UpdatePod(pod *corev1.Pod) {
+	srv.pod.Store(ToPodInfo(pod))
+}
+
+func (srv *ModelServer) GetMetrics() *Metrics {
+	return srv.metrics.Load()
+}
+
+func (srv *ModelServer) UpdateMetrics(metrics *Metrics) {
+	srv.metrics.Store(metrics)
+}
+
+func (srv *ModelServer) Put(key string, value Cloneable) {
+	srv.attributes.Put(key, value)
+}
+
+func (srv *ModelServer) Get(key string) (Cloneable, bool) {
+	return srv.attributes.Get(key)
+}
+
+func (srv *ModelServer) Keys() []string {
+	return srv.attributes.Keys()
+}
+
+func (srv *ModelServer) Clone() *ModelServer {
+	clone := &ModelServer{
+		attributes: srv.attributes.Clone(),
+	}
+	clone.pod.Store(srv.pod.Load().Clone())
+	clone.metrics.Store(srv.metrics.Load().Clone())
+	return clone
+}
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/datalayer/metrics/client.go b/pkg/epp/datalayer/metrics/client.go
new file mode 100644
index 0000000..7961b92
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/client.go
@@ -0,0 +1,83 @@
+/*
+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 metrics
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/prometheus/common/expfmt"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
+)
+
+// Client is an interface for retrieving the metrics from an endpoint URL.
+type Client interface {
+	Get(ctx context.Context, target *url.URL, ep datalayer.Addressable) (PrometheusMetricMap, error)
+}
+
+// -- package implementations --
+const (
+	maxIdleConnections = 5000
+	maxIdleTime        = 10 * time.Second
+	timeout            = 10 * time.Second
+)
+
+var (
+	defaultClient = &client{
+		Client: http.Client{
+			Timeout: timeout,
+			Transport: &http.Transport{
+				MaxIdleConns:        maxIdleConnections,
+				MaxIdleConnsPerHost: 4, // host is defined as scheme://host:port
+			},
+			// TODO: set additional timeouts, transport options, etc.
+		},
+	}
+)
+
+type client struct {
+	http.Client
+}
+
+func (cl *client) Get(ctx context.Context, target *url.URL, ep datalayer.Addressable) (PrometheusMetricMap, error) {
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %v", err)
+	}
+	resp, err := defaultClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to fetch metrics from %s: %w", ep.GetNamespacedName(), err)
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("unexpected status code from %s: %v", ep.GetNamespacedName(), resp.StatusCode)
+	}
+
+	parser := expfmt.TextParser{}
+	metricFamilies, err := parser.TextToMetricFamilies(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	return metricFamilies, err
+}
diff --git a/pkg/epp/datalayer/metrics/datasource.go b/pkg/epp/datalayer/metrics/datasource.go
new file mode 100644
index 0000000..5ff7ef9
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/datasource.go
@@ -0,0 +1,118 @@
+/*
+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 metrics
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"strconv"
+	"sync"
+	"sync/atomic"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
+)
+
+const (
+	dataSourceName = "metrics-data-source"
+)
+
+// DataSource is a Model Server Protocol (MSP) compliant metrics data source,
+// returning Prometheus formatted metrics for an endpoint.
+type DataSource struct {
+	metricsScheme string                 // scheme to use in metrics URL
+	metricsPort   atomic.Pointer[string] // target port to use in metrics URL
+	metricsPath   string                 // path to use in metrics URL
+
+	client     Client   // client (e.g. a wrapped http.Client) used to get metrics
+	extractors sync.Map // key: name, value: extractor
+}
+
+// NewDataSource returns a new MSP compliant metrics data source, configured with the provided
+// client factory. If ClientFactory is nil, a default factory is used.
+func NewDataSource(metricsScheme string, metricsPort int32, metricsPath string, cl Client) *DataSource {
+	if cl == nil {
+		cl = defaultClient
+	}
+
+	dataSrc := &DataSource{
+		metricsScheme: metricsScheme,
+		metricsPath:   metricsPath,
+		client:        cl,
+	}
+	dataSrc.SetPort(metricsPort)
+	return dataSrc
+}
+
+// SetPort updates the port used for metrics scraping.
+func (dataSrc *DataSource) SetPort(metricsPort int32) {
+	port := strconv.Itoa(int(metricsPort))
+	dataSrc.metricsPort.Store(&port)
+}
+
+// Name returns the metrics data source name.
+func (dataSrc *DataSource) Name() string {
+	return dataSourceName
+}
+
+// AddExtractor adds an extractor to the data source, validating it can process
+// the metrics' data source output type.
+func (dataSrc *DataSource) AddExtractor(extractor datalayer.Extractor) error {
+	if err := datalayer.ValidateExtractorType(PrometheusMetricType, extractor.ExpectedInputType()); err != nil {
+		return err
+	}
+	if _, loaded := dataSrc.extractors.LoadOrStore(extractor.Name(), extractor); loaded {
+		return fmt.Errorf("attempt to add extractor with duplicate name %s to %s", extractor.Name(), dataSrc.Name())
+	}
+	return nil
+}
+
+// Collect is triggered by the data layer framework to fetch potentially new
+// MSP metrics data for an endpoint.
+func (dataSrc *DataSource) Collect(ctx context.Context, ep datalayer.Endpoint) error {
+	target := dataSrc.getMetricsEndpoint(ep.GetPod())
+	families, err := dataSrc.client.Get(ctx, target, ep.GetPod())
+
+	if err != nil {
+		return err
+	}
+
+	var errs []error
+	dataSrc.extractors.Range(func(_, val any) bool {
+		if ex, ok := val.(datalayer.Extractor); ok {
+			if err = ex.Extract(ctx, families, ep); err != nil {
+				errs = append(errs, err)
+			}
+		}
+		return true // continue iteration
+	})
+
+	if len(errs) != 0 {
+		return errors.Join(errs...)
+	}
+	return nil
+}
+
+func (dataSrc *DataSource) getMetricsEndpoint(ep datalayer.Addressable) *url.URL {
+	return &url.URL{
+		Scheme: dataSrc.metricsScheme,
+		Host:   net.JoinHostPort(ep.GetIPAddress(), *dataSrc.metricsPort.Load()),
+		Path:   dataSrc.metricsPath,
+	}
+}
diff --git a/pkg/epp/datalayer/metrics/extractor.go b/pkg/epp/datalayer/metrics/extractor.go
new file mode 100644
index 0000000..d7a75b1
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/extractor.go
@@ -0,0 +1,155 @@
+/*
+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 metrics
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	dto "github.com/prometheus/client_model/go"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer"
+)
+
+const (
+	extractorName = "model-server-protocol-metrics"
+
+	// LoRA metrics based on MSP
+	LoraInfoRunningAdaptersMetricName = "running_lora_adapters"
+	LoraInfoWaitingAdaptersMetricName = "waiting_lora_adapters"
+	LoraInfoMaxAdaptersMetricName     = "max_lora"
+)
+
+// Extractor implements the metrics extraction based on the model
+// server protocol standard.
+type Extractor struct {
+	mapping *Mapping
+}
+
+// NewExtractor returns a new model server protocol (MSP) metrics extractor,
+// configured with the given metrics' specifications.
+// These are mandatory metrics per the MSP specification, and are used
+// as the basis for the built-in scheduling plugins.
+func NewExtractor(queueSpec, kvusageSpec, loraSpec string) (*Extractor, error) {
+	mapping, err := NewMapping(queueSpec, kvusageSpec, loraSpec)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create extractor metrics Mapping - %w", err)
+	}
+	return &Extractor{
+		mapping: mapping,
+	}, nil
+}
+
+// Name returns the name of the metrics.Extractor.
+func (ext *Extractor) Name() string {
+	return extractorName
+}
+
+// ExpectedType defines the type expected by the metrics.Extractor - a
+// parsed output from a Prometheus metrics endpoint.
+func (ext *Extractor) ExpectedInputType() reflect.Type {
+	return PrometheusMetricType
+}
+
+// Extract transforms the data source output into a concrete attribute that
+// is stored on the given endpoint.
+func (ext *Extractor) Extract(ctx context.Context, data any, ep datalayer.Endpoint) error {
+	families, ok := data.(PrometheusMetricMap)
+	if !ok {
+		return fmt.Errorf("unexpected input in Extract: %T", data)
+	}
+
+	var errs []error
+	current := ep.GetMetrics()
+	clone := current.Clone()
+	updated := false
+
+	if spec := ext.mapping.TotalQueuedRequests; spec != nil { // extract queued requests
+		if metric, err := spec.getLatestMetric(families); err != nil {
+			errs = append(errs, err)
+		} else {
+			clone.WaitingQueueSize = int(extractValue(metric))
+			updated = true
+		}
+	}
+
+	if spec := ext.mapping.KVCacheUtilization; spec != nil { // extract KV cache usage
+		if metric, err := spec.getLatestMetric(families); err != nil {
+			errs = append(errs, err)
+		} else {
+			clone.KVCacheUsagePercent = extractValue(metric)
+			updated = true
+		}
+	}
+
+	if spec := ext.mapping.LoraRequestInfo; spec != nil { // extract LoRA-specific metrics
+		metric, err := spec.getLatestMetric(families)
+		if err != nil {
+			errs = append(errs, err)
+		} else if metric != nil {
+			populateLoRAMetrics(clone, metric, &errs)
+			updated = true
+		}
+	}
+
+	if updated {
+		clone.UpdateTime = time.Now()
+		ep.UpdateMetrics(clone)
+	}
+
+	if len(errs) != 0 {
+		return errors.Join(errs...)
+	}
+	return nil
+}
+
+// populateLoRAMetrics updates the metrics with LoRA adapter info from the metric labels.
+func populateLoRAMetrics(clone *datalayer.Metrics, metric *dto.Metric, errs *[]error) {
+	clone.ActiveModels = map[string]int{}
+	clone.WaitingModels = map[string]int{}
+
+	for _, label := range metric.GetLabel() {
+		switch label.GetName() {
+		case LoraInfoRunningAdaptersMetricName:
+			addAdapters(clone.ActiveModels, label.GetValue())
+		case LoraInfoWaitingAdaptersMetricName:
+			addAdapters(clone.WaitingModels, label.GetValue())
+		case LoraInfoMaxAdaptersMetricName:
+			if label.GetValue() != "" {
+				if val, err := strconv.Atoi(label.GetValue()); err == nil {
+					clone.MaxActiveModels = val
+				} else {
+					*errs = append(*errs, err)
+				}
+			}
+		}
+	}
+}
+
+// addAdapters splits a comma-separated adapter list and stores keys with default value 0.
+func addAdapters(m map[string]int, csv string) {
+	for _, name := range strings.Split(csv, ",") {
+		if trimmed := strings.TrimSpace(name); trimmed != "" {
+			m[trimmed] = 0
+		}
+	}
+}
diff --git a/pkg/epp/datalayer/metrics/loraspec.go b/pkg/epp/datalayer/metrics/loraspec.go
new file mode 100644
index 0000000..fa9503f
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/loraspec.go
@@ -0,0 +1,73 @@
+/*
+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 metrics
+
+import (
+	"fmt"
+
+	dto "github.com/prometheus/client_model/go"
+)
+
+// LoRASpec extends the standard Spec to allow special case
+// handling for retrieving the latest metrics value for LoRAs.
+type LoRASpec struct {
+	*Spec
+}
+
+// parseStringToLoRASpec parses the metric specification but
+// wraps the return in a LoRASpec.
+func parseStringToLoRASpec(spec string) (*LoRASpec, error) {
+	baseSpec, err := parseStringToSpec(spec)
+	if err != nil {
+		return nil, err
+	}
+	return &LoRASpec{
+		Spec: baseSpec,
+	}, nil
+}
+
+// getLatestMetric retrieves the latest LoRA metric based on Spec.
+// We can't use the standard Spec method since, in the case of
+// LoRA (i.e., `vllm:lora_requests_info`), each label key-value pair permutation
+// generates new series and only most recent should be used. The value of each
+// series is its creation timestamp so we can retrieve the latest by sorting on
+// that the value first.
+func (spec *LoRASpec) getLatestMetric(families PrometheusMetricMap) (*dto.Metric, error) {
+	family, err := extractFamily(spec.Spec, families)
+	if err != nil {
+		return nil, err
+	}
+
+	var latest *dto.Metric
+	var recent float64 = -1
+
+	for _, metric := range family.GetMetric() {
+		if spec.labelsMatch(metric.GetLabel()) {
+			value := extractValue(metric) // metric value is its creation timestamp
+			if value > recent {
+				recent = value
+				latest = metric
+			}
+		}
+	}
+
+	if latest == nil {
+		return nil, fmt.Errorf("no matching lora metric found for %q with labels %v", spec.Name, spec.Labels)
+	}
+
+	return latest, nil
+}
diff --git a/pkg/epp/datalayer/metrics/mapping.go b/pkg/epp/datalayer/metrics/mapping.go
new file mode 100644
index 0000000..1c3c382
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/mapping.go
@@ -0,0 +1,55 @@
+/*
+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 metrics
+
+import (
+	"errors"
+)
+
+// Mapping holds specifications for the well-known metrics defined
+// in the Model Server Protocol.
+type Mapping struct {
+	TotalQueuedRequests *Spec
+	KVCacheUtilization  *Spec
+	LoraRequestInfo     *LoRASpec
+}
+
+// NewMapping creates a metrics.Mapping from the input specification strings.
+func NewMapping(queue, kvusage, lora string) (*Mapping, error) {
+	var errs []error
+
+	queueSpec, err := parseStringToSpec(queue)
+	if err != nil {
+		errs = append(errs, err)
+	}
+	kvusageSpec, err := parseStringToSpec(kvusage)
+	if err != nil {
+		errs = append(errs, err)
+	}
+	loraSpec, err := parseStringToLoRASpec(lora)
+	if err != nil {
+		errs = append(errs, err)
+	}
+	if len(errs) != 0 {
+		return nil, errors.Join(errs...)
+	}
+	return &Mapping{
+		TotalQueuedRequests: queueSpec,
+		KVCacheUtilization:  kvusageSpec,
+		LoraRequestInfo:     loraSpec,
+	}, nil
+}
diff --git a/pkg/epp/datalayer/metrics/spec.go b/pkg/epp/datalayer/metrics/spec.go
new file mode 100644
index 0000000..a079c13
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/spec.go
@@ -0,0 +1,157 @@
+/*
+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 metrics
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+
+	dto "github.com/prometheus/client_model/go"
+	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/promql/parser"
+)
+
+// Spec represents a single metric's specification.
+type Spec struct {
+	Name   string            // the metric's name
+	Labels map[string]string // maps metric's label name to value
+}
+
+// parseStringToSpec converts a string to a metrics.Spec.
+// Inputs are expected in PromQL Instant Vector Selector syntax:
+// metric_name{label1=value1,label2=value2}, where labels are optional.
+func parseStringToSpec(spec string) (*Spec, error) {
+	if spec == "" {
+		return nil, nil // allow empty string to represent the nil Spec
+	}
+
+	// Be liberal in accepting inputs that are missing quotes around label values,
+	// allowing both {label=value} and {label=\"value\"} inputs
+	quoted := addQuotesToLabelValues(spec)
+	expr, err := parser.ParseExpr(quoted)
+	if err != nil {
+		return nil, err
+	}
+
+	// cast to prometheus' VectorSelector to extract metric name and labels
+	if vs, ok := expr.(*parser.VectorSelector); ok {
+		metricLabels := make(map[string]string)
+		for _, matcher := range vs.LabelMatchers {
+			// do not insert pseudo labels (such as __name__, __meta_*, etc.)
+			if matcher.Type == labels.MatchEqual && !strings.HasPrefix(matcher.Name, "__") {
+				metricLabels[matcher.Name] = matcher.Value
+			}
+		}
+
+		if vs.Name == "" {
+			return nil, fmt.Errorf("empty metric name in specification: %q", spec)
+		}
+		return &Spec{
+			Name:   vs.Name,
+			Labels: metricLabels,
+		}, nil
+	}
+
+	return nil, errors.New("not a valid metric specification")
+}
+
+// addQuotesToLabelValues wraps label values with quotes, if missing.
+// TODO: compile regexp once as file scoped var? Seems that this won't be
+// a performance hot spot.
+func addQuotesToLabelValues(input string) string {
+	re := regexp.MustCompile(`(\w+)\s*=\s*([^",}\s]+)`)
+	return re.ReplaceAllString(input, `$1="$2"`)
+}
+
+// extract the metric family is common to standard and LoRA spec's.
+func extractFamily(spec *Spec, families PrometheusMetricMap) (*dto.MetricFamily, error) {
+	if spec == nil {
+		return nil, errors.New("metric specification is nil")
+	}
+
+	family, exists := families[spec.Name]
+	if !exists {
+		return nil, fmt.Errorf("metric family %q not found", spec.Name)
+	}
+
+	if len(family.GetMetric()) == 0 {
+		return nil, fmt.Errorf("no metrics found for %q", spec.Name)
+	}
+	return family, nil
+}
+
+// getLatestMetric retrieves the latest metric based on Spec.
+func (spec *Spec) getLatestMetric(families PrometheusMetricMap) (*dto.Metric, error) {
+	family, err := extractFamily(spec, families)
+	if err != nil {
+		return nil, err
+	}
+
+	var latest *dto.Metric
+	var recent int64 = -1
+
+	for _, metric := range family.GetMetric() {
+		if spec.labelsMatch(metric.GetLabel()) {
+			ts := metric.GetTimestampMs()
+			if ts > recent {
+				recent = ts
+				latest = metric
+			}
+		}
+	}
+
+	if latest == nil {
+		return nil, fmt.Errorf("no matching metric found for %q with labels %v", spec.Name, spec.Labels)
+	}
+
+	return latest, nil
+}
+
+// labelsMatch checks if metric labels match the specification labels.
+func (spec *Spec) labelsMatch(metricLabels []*dto.LabelPair) bool {
+	if len(spec.Labels) == 0 {
+		return true // no label requirements
+	}
+
+	metricLabelMap := make(map[string]string)
+	for _, label := range metricLabels {
+		metricLabelMap[label.GetName()] = label.GetValue()
+	}
+
+	// check if all spec labels match
+	for name, value := range spec.Labels {
+		if metricValue, exists := metricLabelMap[name]; !exists || metricValue != value {
+			return false
+		}
+	}
+
+	return true
+}
+
+// extractValue gets the numeric value from different metric types.
+// Currently only Gauge and Counter are supported.
+func extractValue(metric *dto.Metric) float64 {
+	if gauge := metric.GetGauge(); gauge != nil {
+		return gauge.GetValue()
+	}
+	if counter := metric.GetCounter(); counter != nil {
+		return counter.GetValue()
+	}
+	return 0
+}
diff --git a/pkg/epp/datalayer/metrics/types.go b/pkg/epp/datalayer/metrics/types.go
new file mode 100644
index 0000000..9ee13b3
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/types.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package metrics
+
+import (
+	"reflect"
+
+	dto "github.com/prometheus/client_model/go"
+)
+
+type PrometheusMetricMap = map[string]*dto.MetricFamily
+
+var (
+	PrometheusMetricType = reflect.TypeOf(PrometheusMetricMap{})
+)
diff --git a/pkg/epp/datalayer/mocks/ticker.go b/pkg/epp/datalayer/mocks/ticker.go
new file mode 100644
index 0000000..751704d
--- /dev/null
+++ b/pkg/epp/datalayer/mocks/ticker.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package mocks
+
+import (
+	"time"
+)
+
+// -- Ticker is a mock time source --
+type Ticker struct {
+	ch chan time.Time
+}
+
+func NewTicker() *Ticker {
+	return &Ticker{
+		ch: make(chan time.Time, 10),
+	}
+}
+
+func (t *Ticker) Channel() <-chan time.Time {
+	return t.ch
+}
+
+func (t *Ticker) Tick() {
+	select {
+	case t.ch <- time.Now():
+	default: // if buffer is full, or channel closed
+	}
+}
+
+func (t *Ticker) Stop() {}
diff --git a/pkg/epp/datastore/datastore.go b/pkg/epp/datastore/datastore.go
index eefc1df..00275b9 100644
--- a/pkg/epp/datastore/datastore.go
+++ b/pkg/epp/datastore/datastore.go
@@ -32,14 +32,11 @@ 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"
 )
 
-const (
-	ModelNameIndexKey = "spec.modelName"
-)
-
 var (
 	errPoolNotSynced = errors.New("InferencePool is not initialized in data store")
 )
@@ -56,10 +53,9 @@ type Datastore interface {
 	PoolLabelsMatch(podLabels map[string]string) bool
 
 	// InferenceObjective operations
-	ObjectiveSetIfOlder(infModel *v1alpha2.InferenceObjective) bool
-	ObjectiveGet(modelName string) *v1alpha2.InferenceObjective
-	ObjectiveDelete(namespacedName types.NamespacedName) *v1alpha2.InferenceObjective
-	ObjectiveResync(ctx context.Context, reader client.Reader, modelName string) (bool, error)
+	ObjectiveSet(infObjective *v1alpha2.InferenceObjective)
+	ObjectiveGet(objectiveName string) *v1alpha2.InferenceObjective
+	ObjectiveDelete(namespacedName types.NamespacedName)
 	ObjectiveGetAll() []*v1alpha2.InferenceObjective
 
 	// PodList lists pods matching the given predicate.
@@ -71,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
 }
@@ -92,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() {
@@ -102,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()
@@ -157,78 +153,32 @@ 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)
 }
 
-func (ds *datastore) ObjectiveSetIfOlder(infObjective *v1alpha2.InferenceObjective) bool {
+func (ds *datastore) ObjectiveSet(infObjective *v1alpha2.InferenceObjective) {
 	ds.poolAndObjectivesMu.Lock()
 	defer ds.poolAndObjectivesMu.Unlock()
-
-	// Check first if the existing model is older.
-	// One exception is if the incoming model object is the same, in which case, we should not
-	// check for creation timestamp since that means the object was re-created, and so we should override.
-	existing, exists := ds.objectives[infObjective.Spec.ModelName]
-	if exists {
-		diffObj := infObjective.Name != existing.Name || infObjective.Namespace != existing.Namespace
-		if diffObj && existing.CreationTimestamp.Before(&infObjective.CreationTimestamp) {
-			return false
-		}
-	}
 	// Set the objective.
-	ds.objectives[infObjective.Spec.ModelName] = infObjective
-	return true
-}
-
-func (ds *datastore) ObjectiveResync(ctx context.Context, reader client.Reader, modelName string) (bool, error) {
-	ds.poolAndObjectivesMu.Lock()
-	defer ds.poolAndObjectivesMu.Unlock()
-
-	var objectives v1alpha2.InferenceObjectiveList
-	if err := reader.List(ctx, &objectives, client.MatchingFields{ModelNameIndexKey: modelName}, client.InNamespace(ds.pool.Namespace)); err != nil {
-		return false, fmt.Errorf("listing objectives that match the modelName %s: %w", modelName, err)
-	}
-	if len(objectives.Items) == 0 {
-		// No other instances of InferenceObjectives with this ModelName exists.
-		return false, nil
-	}
-
-	var oldest *v1alpha2.InferenceObjective
-	for i := range objectives.Items {
-		m := &objectives.Items[i]
-		if m.Spec.ModelName != modelName || // The index should filter those out, but just in case!
-			m.Spec.PoolRef.Name != v1alpha2.ObjectName(ds.pool.Name) || // We don't care about other pools, we could setup an index on this too!
-			!m.DeletionTimestamp.IsZero() { // ignore objects marked for deletion
-			continue
-		}
-		if oldest == nil || m.CreationTimestamp.Before(&oldest.CreationTimestamp) {
-			oldest = m
-		}
-	}
-	if oldest == nil {
-		return false, nil
-	}
-	ds.objectives[modelName] = oldest
-	return true, nil
+	ds.objectives[infObjective.Name] = infObjective
 }
 
-func (ds *datastore) ObjectiveGet(modelName string) *v1alpha2.InferenceObjective {
+func (ds *datastore) ObjectiveGet(objectiveName string) *v1alpha2.InferenceObjective {
 	ds.poolAndObjectivesMu.RLock()
 	defer ds.poolAndObjectivesMu.RUnlock()
-	return ds.objectives[modelName]
+	iObj, ok := ds.objectives[objectiveName]
+	if !ok {
+		return nil
+	}
+	return iObj
 }
 
-func (ds *datastore) ObjectiveDelete(namespacedName types.NamespacedName) *v1alpha2.InferenceObjective {
+func (ds *datastore) ObjectiveDelete(namespacedName types.NamespacedName) {
 	ds.poolAndObjectivesMu.Lock()
 	defer ds.poolAndObjectivesMu.Unlock()
-	for _, m := range ds.objectives {
-		if m.Name == namespacedName.Name && m.Namespace == namespacedName.Namespace {
-			delete(ds.objectives, m.Spec.ModelName)
-			return m
-		}
-	}
-	return nil
+	delete(ds.objectives, namespacedName.Name)
 }
 
 func (ds *datastore) ObjectiveGetAll() []*v1alpha2.InferenceObjective {
@@ -266,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)
@@ -279,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))
 	}
 }
 
@@ -288,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 b528ed7..7f81221 100644
--- a/pkg/epp/handlers/request.go
+++ b/pkg/epp/handlers/request.go
@@ -17,7 +17,7 @@ limitations under the License.
 package handlers
 
 import (
-	"context"
+	"fmt"
 	"strconv"
 	"time"
 
@@ -25,10 +25,11 @@ import (
 	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
 	"google.golang.org/protobuf/types/known/structpb"
 
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
 )
 
-func (s *StreamingServer) HandleRequestHeaders(ctx context.Context, reqCtx *RequestContext, req *extProcPb.ProcessingRequest_RequestHeaders) error {
+func (s *StreamingServer) HandleRequestHeaders(reqCtx *RequestContext, req *extProcPb.ProcessingRequest_RequestHeaders) error {
 	reqCtx.RequestReceivedTimestamp = time.Now()
 
 	// an EoS in the request headers means this request has no body or trailers.
@@ -45,7 +46,10 @@ func (s *StreamingServer) HandleRequestHeaders(ctx context.Context, reqCtx *Requ
 		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
@@ -57,12 +61,23 @@ func (s *StreamingServer) HandleRequestHeaders(ctx context.Context, reqCtx *Requ
 		} else {
 			reqCtx.Request.Headers[header.Key] = header.Value
 		}
-		if header.Key == s.fairnessIDHeaderKey {
+		switch header.Key {
+		case metadata.FlowFairnessIDKey:
 			reqCtx.FairnessID = reqCtx.Request.Headers[header.Key]
 			// remove the fairness ID header from the request headers,
 			// this is not data that should be manipulated or sent to the backend.
 			// It is only used for flow control.
 			delete(reqCtx.Request.Headers, header.Key)
+		case metadata.ObjectiveKey:
+			reqCtx.ObjectiveKey = reqCtx.Request.Headers[header.Key]
+			// 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
@@ -108,7 +123,7 @@ func (s *StreamingServer) generateHeaders(reqCtx *RequestContext) []*configPb.He
 	headers := []*configPb.HeaderValueOption{
 		{
 			Header: &configPb.HeaderValue{
-				Key:      s.destinationEndpointHintKey,
+				Key:      metadata.DestinationEndpointKey,
 				RawValue: []byte(reqCtx.TargetEndpoint),
 			},
 		},
@@ -137,27 +152,21 @@ func (s *StreamingServer) generateHeaders(reqCtx *RequestContext) []*configPb.He
 }
 
 func (s *StreamingServer) generateMetadata(endpoint string) *structpb.Struct {
-	targetEndpointValue := &structpb.Struct{
+	return &structpb.Struct{
 		Fields: map[string]*structpb.Value{
-			s.destinationEndpointHintKey: {
-				Kind: &structpb.Value_StringValue{
-					StringValue: endpoint,
-				},
-			},
-		},
-	}
-	dynamicMetadata := targetEndpointValue
-	if s.destinationEndpointHintMetadataNamespace != "" {
-		// If a namespace is defined, wrap the selected endpoint with that.
-		dynamicMetadata = &structpb.Struct{
-			Fields: map[string]*structpb.Value{
-				s.destinationEndpointHintMetadataNamespace: {
-					Kind: &structpb.Value_StructValue{
-						StructValue: targetEndpointValue,
+			metadata.DestinationEndpointNamespace: {
+				Kind: &structpb.Value_StructValue{
+					StructValue: &structpb.Struct{
+						Fields: map[string]*structpb.Value{
+							metadata.DestinationEndpointKey: {
+								Kind: &structpb.Value_StringValue{
+									StringValue: endpoint,
+								},
+							},
+						},
 					},
 				},
 			},
-		}
+		},
 	}
-	return dynamicMetadata
 }
diff --git a/pkg/epp/handlers/response.go b/pkg/epp/handlers/response.go
index 47c0b9d..0ccaf81 100644
--- a/pkg/epp/handlers/response.go
+++ b/pkg/epp/handlers/response.go
@@ -69,8 +69,8 @@ func (s *StreamingServer) HandleResponseBodyModelStreaming(ctx context.Context,
 	if strings.Contains(responseText, streamingEndMsg) {
 		resp := parseRespForUsage(ctx, responseText)
 		reqCtx.Usage = resp.Usage
-		metrics.RecordInputTokens(reqCtx.Model, reqCtx.ResolvedTargetModel, resp.Usage.PromptTokens)
-		metrics.RecordOutputTokens(reqCtx.Model, reqCtx.ResolvedTargetModel, resp.Usage.CompletionTokens)
+		metrics.RecordInputTokens(reqCtx.IncomingModelName, reqCtx.TargetModelName, resp.Usage.PromptTokens)
+		metrics.RecordOutputTokens(reqCtx.IncomingModelName, reqCtx.TargetModelName, resp.Usage.CompletionTokens)
 	}
 }
 
diff --git a/pkg/epp/handlers/server.go b/pkg/epp/handlers/server.go
index 62ffc8c..89e09f9 100644
--- a/pkg/epp/handlers/server.go
+++ b/pkg/epp/handlers/server.go
@@ -44,13 +44,10 @@ const (
 	bodyByteLimit = 62000
 )
 
-func NewStreamingServer(destinationEndpointHintMetadataNamespace, destinationEndpointHintKey, fairnessIDHeaderKey string, datastore Datastore, director Director) *StreamingServer {
+func NewStreamingServer(datastore Datastore, director Director) *StreamingServer {
 	return &StreamingServer{
-		destinationEndpointHintMetadataNamespace: destinationEndpointHintMetadataNamespace,
-		destinationEndpointHintKey:               destinationEndpointHintKey,
-		fairnessIDHeaderKey:                      fairnessIDHeaderKey,
-		director:                                 director,
-		datastore:                                datastore,
+		director:  director,
+		datastore: datastore,
 	}
 }
 
@@ -67,15 +64,8 @@ type Datastore interface {
 // Server implements the Envoy external processing server.
 // https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ext_proc/v3/external_processor.proto
 type StreamingServer struct {
-	// The key of the header to specify the target pod address. This value needs to match Envoy
-	// configuration.
-	destinationEndpointHintKey string
-	// The key acting as the outer namespace struct in the metadata extproc response to communicate
-	// back the picked endpoints.
-	destinationEndpointHintMetadataNamespace string
-	fairnessIDHeaderKey                      string
-	datastore                                Datastore
-	director                                 Director
+	datastore Datastore
+	director  Director
 }
 
 // RequestContext stores context information during the life time of an HTTP request.
@@ -85,9 +75,10 @@ type StreamingServer struct {
 type RequestContext struct {
 	TargetPod                 *backend.Pod
 	TargetEndpoint            string
-	Model                     string
-	ResolvedTargetModel       string
+	IncomingModelName         string
+	TargetModelName           string
 	FairnessID                string
+	ObjectiveKey              string
 	RequestReceivedTimestamp  time.Time
 	ResponseCompleteTimestamp time.Time
 	RequestSize               int
@@ -164,12 +155,12 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 	var err error
 	defer func(error, *RequestContext) {
 		if reqCtx.ResponseStatusCode != "" {
-			metrics.RecordRequestErrCounter(reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.ResponseStatusCode)
+			metrics.RecordRequestErrCounter(reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.ResponseStatusCode)
 		} else if err != nil {
-			metrics.RecordRequestErrCounter(reqCtx.Model, reqCtx.ResolvedTargetModel, errutil.CanonicalCode(err))
+			metrics.RecordRequestErrCounter(reqCtx.IncomingModelName, reqCtx.TargetModelName, errutil.CanonicalCode(err))
 		}
 		if reqCtx.RequestRunning {
-			metrics.DecRunningRequests(reqCtx.Model)
+			metrics.DecRunningRequests(reqCtx.IncomingModelName)
 		}
 	}(err, reqCtx)
 
@@ -200,7 +191,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 				loggerTrace = logger.V(logutil.TRACE)
 				ctx = log.IntoContext(ctx, logger)
 			}
-			err = s.HandleRequestHeaders(ctx, reqCtx, v)
+			err = s.HandleRequestHeaders(reqCtx, v)
 		case *extProcPb.ProcessingRequest_RequestBody:
 			loggerTrace.Info("Incoming body chunk", "EoS", v.RequestBody.EndOfStream)
 			// In the stream case, we can receive multiple request bodies.
@@ -238,8 +229,8 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 				reqCtx.reqHeaderResp = s.generateRequestHeaderResponse(reqCtx)
 				reqCtx.reqBodyResp = s.generateRequestBodyResponses(requestBodyBytes)
 
-				metrics.RecordRequestCounter(reqCtx.Model, reqCtx.ResolvedTargetModel)
-				metrics.RecordRequestSizes(reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.RequestSize)
+				metrics.RecordRequestCounter(reqCtx.IncomingModelName, reqCtx.TargetModelName)
+				metrics.RecordRequestSizes(reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.RequestSize)
 			}
 		case *extProcPb.ProcessingRequest_RequestTrailers:
 			// This is currently unused.
@@ -278,8 +269,8 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 					loggerTrace.Info("stream completed")
 
 					reqCtx.ResponseCompleteTimestamp = time.Now()
-					metrics.RecordRequestLatencies(ctx, reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.RequestReceivedTimestamp, reqCtx.ResponseCompleteTimestamp)
-					metrics.RecordResponseSizes(reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.ResponseSize)
+					metrics.RecordRequestLatencies(ctx, reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.RequestReceivedTimestamp, reqCtx.ResponseCompleteTimestamp)
+					metrics.RecordResponseSizes(reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.ResponseSize)
 				}
 
 				reqCtx.respBodyResp = generateResponseBodyResponses(v.ResponseBody.Body, v.ResponseBody.EndOfStream)
@@ -313,10 +304,10 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
 						}
 					} else if reqCtx.ResponseComplete {
 						reqCtx.ResponseCompleteTimestamp = time.Now()
-						metrics.RecordRequestLatencies(ctx, reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.RequestReceivedTimestamp, reqCtx.ResponseCompleteTimestamp)
-						metrics.RecordResponseSizes(reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.ResponseSize)
-						metrics.RecordInputTokens(reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.Usage.PromptTokens)
-						metrics.RecordOutputTokens(reqCtx.Model, reqCtx.ResolvedTargetModel, reqCtx.Usage.CompletionTokens)
+						metrics.RecordRequestLatencies(ctx, reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.RequestReceivedTimestamp, reqCtx.ResponseCompleteTimestamp)
+						metrics.RecordResponseSizes(reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.ResponseSize)
+						metrics.RecordInputTokens(reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.Usage.PromptTokens)
+						metrics.RecordOutputTokens(reqCtx.IncomingModelName, reqCtx.TargetModelName, reqCtx.Usage.CompletionTokens)
 					}
 				}
 			}
@@ -370,7 +361,7 @@ func (r *RequestContext) updateStateAndSendIfNeeded(srv extProcPb.ExternalProces
 			}
 		}
 		r.RequestState = BodyRequestResponsesComplete
-		metrics.IncRunningRequests(r.Model)
+		metrics.IncRunningRequests(r.IncomingModelName)
 		r.RequestRunning = true
 		// Dump the response so a new stream message can begin
 		r.reqBodyResp = nil
diff --git a/pkg/epp/metadata/consts.go b/pkg/epp/metadata/consts.go
new file mode 100644
index 0000000..be6c482
--- /dev/null
+++ b/pkg/epp/metadata/consts.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 metadata
+
+const (
+	// SubsetFilterNamespace is the key for the outer namespace struct in the metadata field of the extproc request that is used to wrap the subset filter.
+	SubsetFilterNamespace = "envoy.lb.subset_hint"
+	// SubsetFilterKey is the metadata key used by Envoy to specify an array candidate pods for serving the request.
+	// If not specified, all the pods that are associated with the pool are candidates.
+	SubsetFilterKey = "x-gateway-destination-endpoint-subset"
+	// DestinationEndpointNamespace is the key for the outer namespace struct in the metadata field of the extproc response that is used to wrap the target endpoint.
+	DestinationEndpointNamespace = "envoy.lb"
+	// DestinationEndpointKey is the header and response metadata key used by Envoy to route to the appropriate pod.
+	DestinationEndpointKey = "x-gateway-destination-endpoint"
+	// FlowFairnessIDKey is the header key used to pass the fairness ID to be used in Flow Control.
+	FlowFairnessIDKey = "x-gateway-inference-fairness-id"
+	// ObjectiveKey is the header key used to specify the objective of an incoming request.
+	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 d2073eb..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"
@@ -35,6 +34,7 @@ import (
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
@@ -42,11 +42,6 @@ import (
 	requtil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/request"
 )
 
-const (
-	subsetHintNamespace = "envoy.lb.subset_hint"
-	subsetHintKey       = "x-gateway-destination-endpoint-subset"
-)
-
 // Scheduler defines the interface required by the Director for scheduling.
 type Scheduler interface {
 	Schedule(ctx context.Context, request *schedulingtypes.LLMRequest, candidatePods []schedulingtypes.Pod) (result *schedulingtypes.SchedulingResult, err error)
@@ -75,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:
@@ -88,58 +87,51 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
 	logger := log.FromContext(ctx)
 
 	// --- 1. Parse Request, Resolve Target Models, and Determine Parameters ---
-	var ok bool
 	requestBodyMap := reqCtx.Request.Body
-	reqCtx.Model, ok = requestBodyMap["model"].(string)
+	var ok bool
+	reqCtx.IncomingModelName, ok = requestBodyMap["model"].(string)
+
 	if !ok {
 		return reqCtx, errutil.Error{Code: errutil.BadRequest, Msg: "model not found in request body"}
 	}
+	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
 	}
-
-	modelObj := d.datastore.ObjectiveGet(reqCtx.Model)
-	if modelObj == nil {
-		logger.Info("No associated InferenceObjective found, using default", "model", reqCtx.Model)
-		sheddable := v1alpha2.Sheddable
-		modelObj = &v1alpha2.InferenceObjective{
+	infObjective := d.datastore.ObjectiveGet(reqCtx.ObjectiveKey)
+	if infObjective == nil {
+		logger.V(logutil.VERBOSE).Info("No associated InferenceObjective found, using default", "objectiveKey", reqCtx.ObjectiveKey)
+		infObjective = &v1alpha2.InferenceObjective{
 			Spec: v1alpha2.InferenceObjectiveSpec{
-				ModelName:   reqCtx.Model,
-				Criticality: &sheddable,
+				Priority: &d.defaultPriority,
 			},
 		}
-	}
-
-	reqCtx.ResolvedTargetModel = reqCtx.Model
-	if len(modelObj.Spec.TargetModels) > 0 {
-		reqCtx.ResolvedTargetModel = RandomWeightedDraw(logger, modelObj, 0)
-		if reqCtx.ResolvedTargetModel == "" {
-			return reqCtx, errutil.Error{Code: errutil.BadConfiguration, Msg: fmt.Sprintf("error getting target model name for model %v", modelObj.Name)}
-		}
-		reqCtx.Request.Body["model"] = reqCtx.ResolvedTargetModel // Update target model in the body.
-	}
-
-	requestCriticality := v1alpha2.Standard
-	if modelObj.Spec.Criticality != nil {
-		requestCriticality = *modelObj.Spec.Criticality
+	} 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)
 	reqCtx.SchedulingRequest = &schedulingtypes.LLMRequest{
 		RequestId:   reqCtx.Request.Headers[requtil.RequestIdHeaderKey],
-		TargetModel: reqCtx.ResolvedTargetModel,
+		TargetModel: reqCtx.TargetModelName,
 		Prompt:      prompt,
 		Headers:     reqCtx.Request.Headers,
 	}
 
-	logger = logger.WithValues("model", reqCtx.Model, "resolvedTargetModel", reqCtx.ResolvedTargetModel, "criticality", requestCriticality)
+	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, requestCriticality, reqCtx.FairnessID); err != nil {
+	if err := d.admitRequest(ctx, *infObjective.Spec.Priority, reqCtx.FairnessID); err != nil {
 		return reqCtx, err
 	}
 
@@ -165,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",
 		}
 	}
 
@@ -196,15 +190,15 @@ func (d *Director) admitRequest(ctx context.Context, requestCriticality v1alpha2
 func (d *Director) getCandidatePodsForScheduling(ctx context.Context, requestMetadata map[string]any) []schedulingtypes.Pod {
 	loggerTrace := log.FromContext(ctx).V(logutil.TRACE)
 
-	subsetMap, found := requestMetadata[subsetHintNamespace].(map[string]any)
+	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[subsetHintKey].([]any)
+	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{}
@@ -246,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 {
@@ -257,7 +254,7 @@ func (d *Director) prepareRequest(ctx context.Context, reqCtx *handlers.RequestC
 	}
 
 	multiEndpointString := strings.Join(targetEndpoints, ",")
-	logger.V(logutil.DEFAULT).Info("Request handled", "model", reqCtx.Model, "targetModel", reqCtx.ResolvedTargetModel, "endpoint", multiEndpointString)
+	logger.V(logutil.VERBOSE).Info("Request handled", "objectiveKey", reqCtx.ObjectiveKey, "incomingModelName", reqCtx.IncomingModelName, "targetModel", reqCtx.TargetModelName, "endpoint", multiEndpointString)
 
 	reqCtx.TargetPod = targetPods[0]
 	reqCtx.TargetEndpoint = multiEndpointString
@@ -290,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
 	}
@@ -299,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/framework/plugins/picker/max_score_picker.go b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
index e7ee333..a0e973f 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/max_score_picker.go
@@ -60,15 +60,13 @@ func NewMaxScorePicker(maxNumOfEndpoints int) *MaxScorePicker {
 	return &MaxScorePicker{
 		typedName:         plugins.TypedName{Type: MaxScorePickerType, Name: MaxScorePickerType},
 		maxNumOfEndpoints: maxNumOfEndpoints,
-		randomGenerator:   rand.New(rand.NewSource(time.Now().UnixNano())),
 	}
 }
 
 // MaxScorePicker picks pod(s) with the maximum score from the list of candidates.
 type MaxScorePicker struct {
 	typedName         plugins.TypedName
-	maxNumOfEndpoints int        // maximum number of endpoints to pick
-	randomGenerator   *rand.Rand // randomGenerator for randomly pick endpoint on tie-break
+	maxNumOfEndpoints int // maximum number of endpoints to pick
 }
 
 // WithName sets the picker's name
@@ -87,8 +85,13 @@ func (p *MaxScorePicker) Pick(ctx context.Context, cycleState *types.CycleState,
 	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting maximum '%d' pods from %d candidates sorted by max score: %+v", p.maxNumOfEndpoints,
 		len(scoredPods), scoredPods))
 
+	// TODO: merge this with the logic in RandomPicker
+	// Rand package is not safe for concurrent use, so we create a new instance.
+	// Source: https://pkg.go.dev/math/rand#pkg-overview
+	randomGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
+
 	// Shuffle in-place - needed for random tie break when scores are equal
-	p.randomGenerator.Shuffle(len(scoredPods), func(i, j int) {
+	randomGenerator.Shuffle(len(scoredPods), func(i, j int) {
 		scoredPods[i], scoredPods[j] = scoredPods[j], scoredPods[i]
 	})
 
@@ -113,5 +116,4 @@ func (p *MaxScorePicker) Pick(ctx context.Context, cycleState *types.CycleState,
 	}
 
 	return &types.ProfileRunResult{TargetPods: targetPods}
-
 }
diff --git a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
index eb62c37..b571ce3 100644
--- a/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
+++ b/pkg/epp/scheduling/framework/plugins/picker/random_picker.go
@@ -58,7 +58,6 @@ func NewRandomPicker(maxNumOfEndpoints int) *RandomPicker {
 	return &RandomPicker{
 		typedName:         plugins.TypedName{Type: RandomPickerType, Name: RandomPickerType},
 		maxNumOfEndpoints: maxNumOfEndpoints,
-		randomGenerator:   rand.New(rand.NewSource(time.Now().UnixNano())),
 	}
 }
 
@@ -66,7 +65,6 @@ func NewRandomPicker(maxNumOfEndpoints int) *RandomPicker {
 type RandomPicker struct {
 	typedName         plugins.TypedName
 	maxNumOfEndpoints int
-	randomGenerator   *rand.Rand // randomGenerator for randomly pick endpoint on tie-break
 }
 
 // WithName sets the name of the picker.
@@ -85,8 +83,13 @@ func (p *RandomPicker) Pick(ctx context.Context, _ *types.CycleState, scoredPods
 	log.FromContext(ctx).V(logutil.DEBUG).Info(fmt.Sprintf("Selecting maximum '%d' pods from %d candidates randomly: %+v", p.maxNumOfEndpoints,
 		len(scoredPods), scoredPods))
 
+	// TODO: merge this with the logic in MaxScorePicker
+	// Rand package is not safe for concurrent use, so we create a new instance.
+	// Source: https://pkg.go.dev/math/rand#pkg-overview
+	randomGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
+
 	// Shuffle in-place
-	p.randomGenerator.Shuffle(len(scoredPods), func(i, j int) {
+	randomGenerator.Shuffle(len(scoredPods), func(i, j int) {
 		scoredPods[i], scoredPods[j] = scoredPods[j], scoredPods[i]
 	})
 
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/pkg/epp/server/runserver.go b/pkg/epp/server/runserver.go
index 629af48..1b84bd7 100644
--- a/pkg/epp/server/runserver.go
+++ b/pkg/epp/server/runserver.go
@@ -45,20 +45,17 @@ import (
 
 // ExtProcServerRunner provides methods to manage an external process server.
 type ExtProcServerRunner struct {
-	GrpcPort                                 int
-	DestinationEndpointHintMetadataNamespace string
-	DestinationEndpointHintKey               string
-	FairnessIDHeaderKey                      string
-	PoolNamespacedName                       types.NamespacedName
-	PoolGKNN                                 common.GKNN
-	Datastore                                datastore.Datastore
-	SecureServing                            bool
-	HealthChecking                           bool
-	CertPath                                 string
-	RefreshPrometheusMetricsInterval         time.Duration
-	MetricsStalenessThreshold                time.Duration
-	Director                                 *requestcontrol.Director
-	SaturationDetector                       requestcontrol.SaturationDetector
+	GrpcPort                         int
+	PoolNamespacedName               types.NamespacedName
+	PoolGKNN                         common.GKNN
+	Datastore                        datastore.Datastore
+	SecureServing                    bool
+	HealthChecking                   bool
+	CertPath                         string
+	RefreshPrometheusMetricsInterval time.Duration
+	MetricsStalenessThreshold        time.Duration
+	Director                         *requestcontrol.Director
+	SaturationDetector               requestcontrol.SaturationDetector
 
 	// This should only be used in tests. We won't need this once we do not inject metrics in the tests.
 	// TODO:(https://github.com/kubernetes-sigs/gateway-api-inference-extension/issues/432) Cleanup
@@ -67,27 +64,24 @@ type ExtProcServerRunner struct {
 
 // Default values for CLI flags in main
 const (
-	DefaultGrpcPort                                 = 9002                              // default for --grpc-port
-	DefaultGrpcHealthPort                           = 9003                              // default for --grpc-health-port
-	DefaultMetricsPort                              = 9090                              // default for --metrics-port
-	DefaultDestinationEndpointHintMetadataNamespace = "envoy.lb"                        // default for --destinationEndpointHintMetadataNamespace
-	DefaultDestinationEndpointHintKey               = "x-gateway-destination-endpoint"  // default for --destination-endpoint-hint-key
-	DefaultFairnessIDHeaderKey                      = "x-gateway-inference-fairness-id" // default for --fairness-id-header-key
-	DefaultPoolName                                 = ""                                // required but no default
-	DefaultPoolNamespace                            = "default"                         // default for --pool-namespace
-	DefaultRefreshMetricsInterval                   = 50 * time.Millisecond             // default for --refresh-metrics-interval
-	DefaultRefreshPrometheusMetricsInterval         = 5 * time.Second                   // default for --refresh-prometheus-metrics-interval
-	DefaultSecureServing                            = true                              // default for --secure-serving
-	DefaultHealthChecking                           = false                             // default for --health-checking
-	DefaultEnablePprof                              = true                              // default for --enable-pprof
-	DefaultTotalQueuedRequestsMetric                = "vllm:num_requests_waiting"       // default for --total-queued-requests-metric
-	DefaultKvCacheUsagePercentageMetric             = "vllm:gpu_cache_usage_perc"       // default for --kv-cache-usage-percentage-metric
-	DefaultLoraInfoMetric                           = "vllm:lora_requests_info"         // default for --lora-info-metric
-	DefaultCertPath                                 = ""                                // default for --cert-path
-	DefaultConfigFile                               = ""                                // default for --config-file
-	DefaultConfigText                               = ""                                // default for --config-text
-	DefaultPoolGroup                                = "inference.networking.k8s.io"     // default for --pool-group
-	DefaultMetricsStalenessThreshold                = 2 * time.Second
+	DefaultGrpcPort                         = 9002                          // default for --grpc-port
+	DefaultGrpcHealthPort                   = 9003                          // default for --grpc-health-port
+	DefaultMetricsPort                      = 9090                          // default for --metrics-port
+	DefaultPoolName                         = ""                            // required but no default
+	DefaultPoolNamespace                    = "default"                     // default for --pool-namespace
+	DefaultRefreshMetricsInterval           = 50 * time.Millisecond         // default for --refresh-metrics-interval
+	DefaultRefreshPrometheusMetricsInterval = 5 * time.Second               // default for --refresh-prometheus-metrics-interval
+	DefaultSecureServing                    = true                          // default for --secure-serving
+	DefaultHealthChecking                   = false                         // default for --health-checking
+	DefaultEnablePprof                      = true                          // default for --enable-pprof
+	DefaultTotalQueuedRequestsMetric        = "vllm:num_requests_waiting"   // default for --total-queued-requests-metric
+	DefaultKvCacheUsagePercentageMetric     = "vllm:gpu_cache_usage_perc"   // default for --kv-cache-usage-percentage-metric
+	DefaultLoraInfoMetric                   = "vllm:lora_requests_info"     // default for --lora-info-metric
+	DefaultCertPath                         = ""                            // default for --cert-path
+	DefaultConfigFile                       = ""                            // default for --config-file
+	DefaultConfigText                       = ""                            // default for --config-text
+	DefaultPoolGroup                        = "inference.networking.k8s.io" // default for --pool-group
+	DefaultMetricsStalenessThreshold        = 2 * time.Second
 )
 
 // NewDefaultExtProcServerRunner creates a runner with default values.
@@ -101,16 +95,13 @@ func NewDefaultExtProcServerRunner() *ExtProcServerRunner {
 		},
 	}
 	return &ExtProcServerRunner{
-		GrpcPort:                                 DefaultGrpcPort,
-		DestinationEndpointHintKey:               DefaultDestinationEndpointHintKey,
-		DestinationEndpointHintMetadataNamespace: DefaultDestinationEndpointHintMetadataNamespace,
-		FairnessIDHeaderKey:                      DefaultFairnessIDHeaderKey,
-		PoolNamespacedName:                       types.NamespacedName{Name: DefaultPoolName, Namespace: DefaultPoolNamespace},
-		PoolGKNN:                                 poolGKNN,
-		SecureServing:                            DefaultSecureServing,
-		HealthChecking:                           DefaultHealthChecking,
-		RefreshPrometheusMetricsInterval:         DefaultRefreshPrometheusMetricsInterval,
-		MetricsStalenessThreshold:                DefaultMetricsStalenessThreshold,
+		GrpcPort:                         DefaultGrpcPort,
+		PoolNamespacedName:               types.NamespacedName{Name: DefaultPoolName, Namespace: DefaultPoolNamespace},
+		PoolGKNN:                         poolGKNN,
+		SecureServing:                    DefaultSecureServing,
+		HealthChecking:                   DefaultHealthChecking,
+		RefreshPrometheusMetricsInterval: DefaultRefreshPrometheusMetricsInterval,
+		MetricsStalenessThreshold:        DefaultMetricsStalenessThreshold,
 		// Dependencies can be assigned later.
 	}
 }
@@ -172,17 +163,8 @@ func (r *ExtProcServerRunner) AsRunnable(logger logr.Logger) manager.Runnable {
 			srv = grpc.NewServer()
 		}
 
-		extProcServer := handlers.NewStreamingServer(
-			r.DestinationEndpointHintMetadataNamespace,
-			r.DestinationEndpointHintKey,
-			r.FairnessIDHeaderKey,
-			r.Datastore,
-			r.Director,
-		)
-		extProcPb.RegisterExternalProcessorServer(
-			srv,
-			extProcServer,
-		)
+		extProcServer := handlers.NewStreamingServer(r.Datastore, r.Director)
+		extProcPb.RegisterExternalProcessorServer(srv, extProcServer)
 
 		if r.HealthChecking {
 			healthcheck := health.NewServer()
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 3eb4d42..0936d29 100644
--- a/site-src/guides/adapter-rollout.md
+++ b/site-src/guides/adapter-rollout.md
@@ -68,8 +68,7 @@ kind: InferenceModel
 metadata:
   name: food-review
 spec:
-  modelName: food-review
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: vllm-llama3-8b-instruct
   targetModels:
@@ -107,8 +106,7 @@ kind: InferenceModel
 metadata:
   name: food-review
 spec:
-  modelName: food-review
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: vllm-llama3-8b-instruct
   targetModels:
diff --git a/site-src/guides/epp-configuration/config-text.md b/site-src/guides/epp-configuration/config-text.md
index 4b1b35d..6df19db 100644
--- a/site-src/guides/epp-configuration/config-text.md
+++ b/site-src/guides/epp-configuration/config-text.md
@@ -81,7 +81,8 @@ The fields in a schedulingProfile entry are:
 - *plugins* specifies the set of plugins to be used when this scheduling profile is chosen for a request.
 Each entry in the schedulingProfile's plugins section has the following fields:
   - *pluginRef* is a reference to the name of the plugin instance to be used
-  - *weight* is the weight to be used if the referenced plugin is a scorer.
+  - *weight* is the weight to be used if the referenced plugin is a scorer. If omitted, a weight of one
+    will be used.
 
 A complete configuration might look like this:
 ```yaml
@@ -93,12 +94,9 @@ plugins:
     hashBlockSize: 5
     maxPrefixBlocksToMatch: 256
     lruCapacityPerServer: 31250
-- type: max-score-picker
-- type: single-profile-handler
 schedulingProfiles:
 - name: default
   plugins:
-  - pluginRef: max-score-picker
   - pluginRef: prefix-cache-scorer
     weight: 50
 ```
@@ -163,16 +161,35 @@ spec:
               hashBlockSize: 5
               maxPrefixBlocksToMatch: 256
               lruCapacityPerServer: 31250
-          - type: max-score-picker
-          - type: single-profile-handler
           schedulingProfiles:
           - name: default
             plugins:
-            - pluginRef: max-score-picker
             - pluginRef: prefix-cache-scorer
               weight: 50
 ```
 
+The EPP configuration in the above two examples are equivalent to the following configuration after
+defaults have been applied:
+
+```yaml
+apiVersion: inference.networking.x-k8s.io/v1alpha1
+kind: EndpointPickerConfig
+plugins:
+- type: prefix-cache-scorer
+  parameters:
+    hashBlockSize: 5
+    maxPrefixBlocksToMatch: 256
+    lruCapacityPerServer: 31250
+- type: single-profile-handler
+- type: max-score-picker
+schedulingProfiles:
+- name: default
+  plugins:
+  - pluginRef: prefix-cache-scorer
+    weight: 50
+  -pluginRef: max-score-picker
+```
+
 ## Plugin Configuration
 
 This section describes how to setup the various plugins that are available with the IGW.
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 a1b10ed..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).
@@ -209,7 +201,7 @@ This quickstart guide is intended for engineers familiar with k8s and model serv
       2. Set the Kgateway version and install the Kgateway CRDs.
 
          ```bash
-         KGTW_VERSION=v2.0.3
+         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
          ```
 
@@ -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 7e99193..98b3cc4 100644
--- a/site-src/guides/inferencepool-rollout.md
+++ b/site-src/guides/inferencepool-rollout.md
@@ -56,8 +56,7 @@ kind: InferenceModel
 metadata:
   name: food-review-new
 spec:
-  modelName: food-review
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: vllm-llama3-8b-instruct-new
   targetModels:
@@ -209,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:
@@ -307,8 +307,6 @@ data:
     - type: queue-scorer
     - type: kv-cache-utilization-scorer
     - type: prefix-cache-scorer
-    - type: max-score-picker
-    - type: single-profile-handler
     schedulingProfiles:
     - name: default
       plugins:
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 d1090b3..59cfe72 100644
--- a/site-src/guides/serve-multiple-lora-adapters.md
+++ b/site-src/guides/serve-multiple-lora-adapters.md
@@ -27,8 +27,7 @@ kind: InferenceModel
 metadata:
   name: english-bot
 spec:
-  modelName: english-bot
-  criticality: Standard
+  criticality: 1
   poolRef:
     name: gemma3
         
@@ -38,8 +37,7 @@ kind: InferenceModel
 metadata:
   name: spanish-bot
 spec:
-  modelName: spanish-bot
-  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 950c083..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,16 +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 feature-rich, Kubernetes-native
-ingress controller and next-generation API gateway. Kgateway brings the
-full power and community support of Gateway API to its existing control-plane
-implementation.
-
-Progress towards supporting this project is tracked with a [GitHub
-Issue](https://github.com/kgateway-dev/kgateway/issues/10411).
-
 ## Google Kubernetes Engine
 
 [Google Kubernetes Engine (GKE)][gke] is a managed Kubernetes platform offered
@@ -68,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 dbed66d..93f3e86 100644
--- a/conformance/tests/epp_unavailable_fail_open.yaml
+++ b/conformance/tests/epp_unavailable_fail_open.yaml
@@ -6,8 +6,7 @@ metadata:
   name: conformance-fake-model-server
   namespace: gateway-conformance-app-backend
 spec:
-  modelName: conformance-fake-model
-  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 839b37f..37b00e7 100644
--- a/conformance/tests/gateway_following_epp_routing.yaml
+++ b/conformance/tests/gateway_following_epp_routing.yaml
@@ -6,8 +6,7 @@ metadata:
   name: conformance-fake-model-server
   namespace: gateway-conformance-app-backend
 spec:
-  modelName: conformance-fake-model
-  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/config/loader/configloader_test.go b/pkg/epp/config/loader/configloader_test.go
index 71f75d6..0701f96 100644
--- a/pkg/epp/config/loader/configloader_test.go
+++ b/pkg/epp/config/loader/configloader_test.go
@@ -27,12 +27,14 @@ import (
 	"k8s.io/utils/ptr"
 
 	configapi "sigs.k8s.io/gateway-api-inference-extension/apix/config/v1alpha1"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/config"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/multi/prefix"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/picker"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/framework/plugins/profile"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 	"sigs.k8s.io/gateway-api-inference-extension/test/utils"
 )
 
@@ -43,7 +45,116 @@ const (
 	testPickerType         = "test-picker"
 )
 
-func TestLoadConfiguration(t *testing.T) {
+type testStruct struct {
+	name       string
+	configText string
+	configFile string
+	want       *configapi.EndpointPickerConfig
+	wantErr    bool
+}
+
+func TestLoadRawConfiguration(t *testing.T) {
+	registerTestPlugins()
+
+	goodConfig := &configapi.EndpointPickerConfig{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "EndpointPickerConfig",
+			APIVersion: "inference.networking.x-k8s.io/v1alpha1",
+		},
+		Plugins: []configapi.PluginSpec{
+			{
+				Name:       "test1",
+				Type:       test1Type,
+				Parameters: json.RawMessage("{\"threshold\":10}"),
+			},
+			{
+				Name: "profileHandler",
+				Type: "test-profile-handler",
+			},
+			{
+				Type:       test2Type,
+				Parameters: json.RawMessage("{\"hashBlockSize\":32}"),
+			},
+			{
+				Name: "testPicker",
+				Type: testPickerType,
+			},
+		},
+		SchedulingProfiles: []configapi.SchedulingProfile{
+			{
+				Name: "default",
+				Plugins: []configapi.SchedulingPlugin{
+					{
+						PluginRef: "test1",
+					},
+					{
+						PluginRef: "test-two",
+						Weight:    ptr.To(50),
+					},
+					{
+						PluginRef: "testPicker",
+					},
+				},
+			},
+		},
+	}
+
+	goodConfigNoProfiles := &configapi.EndpointPickerConfig{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "EndpointPickerConfig",
+			APIVersion: "inference.networking.x-k8s.io/v1alpha1",
+		},
+		Plugins: []configapi.PluginSpec{
+			{
+				Name:       "test1",
+				Type:       test1Type,
+				Parameters: json.RawMessage("{\"threshold\":10}"),
+			},
+		},
+	}
+
+	tests := []testStruct{
+		{
+			name:       "success",
+			configText: successConfigText,
+			configFile: "",
+			want:       goodConfig,
+			wantErr:    false,
+		},
+		{
+			name:       "successNoProfiles",
+			configText: successNoProfilesText,
+			configFile: "",
+			want:       goodConfigNoProfiles,
+			wantErr:    false,
+		},
+		{
+			name:       "errorBadYaml",
+			configText: errorBadYamlText,
+			configFile: "",
+			wantErr:    true,
+		},
+		{
+			name:       "successFromFile",
+			configText: "",
+			configFile: "../../../../test/testdata/configloader_1_test.yaml",
+			want:       goodConfig,
+			wantErr:    false,
+		},
+	}
+
+	for _, test := range tests {
+		configBytes := []byte(test.configText)
+		if test.configFile != "" {
+			configBytes, _ = os.ReadFile(test.configFile)
+		}
+
+		got, err := loadRawConfig(configBytes)
+		checker(t, "loadRawConfig", test, got, err)
+	}
+}
+
+func TestLoadRawConfigurationWithDefaults(t *testing.T) {
 	registerTestPlugins()
 
 	goodConfig := &configapi.EndpointPickerConfig{
@@ -101,6 +212,14 @@ func TestLoadConfiguration(t *testing.T) {
 				Type:       test1Type,
 				Parameters: json.RawMessage("{\"threshold\":10}"),
 			},
+			{
+				Name: profile.SingleProfileHandlerType,
+				Type: profile.SingleProfileHandlerType,
+			},
+			{
+				Name: picker.MaxScorePickerType,
+				Type: picker.MaxScorePickerType,
+			},
 		},
 		SchedulingProfiles: []configapi.SchedulingProfile{
 			{
@@ -109,18 +228,15 @@ func TestLoadConfiguration(t *testing.T) {
 					{
 						PluginRef: "test1",
 					},
+					{
+						PluginRef: "max-score-picker",
+					},
 				},
 			},
 		},
 	}
 
-	tests := []struct {
-		name       string
-		configText string
-		configFile string
-		want       *configapi.EndpointPickerConfig
-		wantErr    bool
-	}{
+	tests := []testStruct{
 		{
 			name:       "success",
 			configText: successConfigText,
@@ -135,12 +251,6 @@ func TestLoadConfiguration(t *testing.T) {
 			want:       goodConfigNoProfiles,
 			wantErr:    false,
 		},
-		{
-			name:       "errorBadYaml",
-			configText: errorBadYamlText,
-			configFile: "",
-			wantErr:    true,
-		},
 		{
 			name:       "errorBadPluginReferenceText",
 			configText: errorBadPluginReferenceText,
@@ -159,12 +269,6 @@ func TestLoadConfiguration(t *testing.T) {
 			configFile: "",
 			wantErr:    true,
 		},
-		{
-			name:       "errorNoProfilePlugins",
-			configText: errorNoProfilePluginsText,
-			configFile: "",
-			wantErr:    true,
-		},
 		{
 			name:       "errorBadProfilePlugin",
 			configText: errorBadProfilePluginText,
@@ -199,31 +303,51 @@ func TestLoadConfiguration(t *testing.T) {
 	}
 
 	for _, test := range tests {
+		handle := utils.NewTestHandle(context.Background())
+
 		configBytes := []byte(test.configText)
 		if test.configFile != "" {
 			configBytes, _ = os.ReadFile(test.configFile)
 		}
 
-		got, err := LoadConfig(configBytes, utils.NewTestHandle(context.Background()))
-		if err != nil {
-			if !test.wantErr {
-				t.Fatalf("In test '%s' LoadConfig returned unexpected error: %v, want %v", test.name, err, test.wantErr)
-			}
-			t.Logf("error was %s", err)
-		} else {
-			if test.wantErr {
-				t.Fatalf("In test %s LoadConfig did not return an expected error", test.name)
-			}
-			if diff := cmp.Diff(test.want, got); diff != "" {
-				t.Errorf("In test %s LoadConfig returned unexpected response, diff(-want, +got): %v", test.name, diff)
+		got, err := loadRawConfig(configBytes)
+		if err == nil {
+			setDefaultsPhaseOne(got)
+
+			err = instantiatePlugins(got.Plugins, handle)
+			if err == nil {
+				setDefaultsPhaseTwo(got, handle)
+
+				err = validateSchedulingProfiles(got)
 			}
 		}
+		checker(t, "tested function", test, got, err)
+	}
+}
+
+func checker(t *testing.T, function string, test testStruct, got *configapi.EndpointPickerConfig, err error) {
+	checkError(t, function, test, err)
+	if err == nil && !test.wantErr {
+		if diff := cmp.Diff(test.want, got); diff != "" {
+			t.Errorf("In test %s %s returned unexpected response, diff(-want, +got): %v", test.name, function, diff)
+		}
+	}
+}
+
+func checkError(t *testing.T, function string, test testStruct, err error) {
+	if err != nil {
+		if !test.wantErr {
+			t.Fatalf("In test '%s' %s returned unexpected error: %v, want %v", test.name, function, err, test.wantErr)
+		}
+		t.Logf("error was %s", err)
+	} else if test.wantErr {
+		t.Fatalf("In test %s %s did not return an expected error", test.name, function)
 	}
 }
 
 func TestInstantiatePlugins(t *testing.T) {
 	handle := utils.NewTestHandle(context.Background())
-	_, err := LoadConfig([]byte(successConfigText), handle)
+	_, err := LoadConfig([]byte(successConfigText), handle, logging.NewTestLogger())
 	if err != nil {
 		t.Fatalf("LoadConfig returned unexpected error - %v", err)
 	}
@@ -237,16 +361,18 @@ func TestInstantiatePlugins(t *testing.T) {
 	}
 
 	handle = utils.NewTestHandle(context.Background())
-	_, err = LoadConfig([]byte(errorBadPluginReferenceParametersText), handle)
+	_, err = LoadConfig([]byte(errorBadPluginReferenceParametersText), handle, logging.NewTestLogger())
 	if err == nil {
 		t.Fatalf("LoadConfig did not return error as expected ")
 	}
 }
 
-func TestLoadSchedulerConfig(t *testing.T) {
+func TestLoadConfig(t *testing.T) {
+
 	tests := []struct {
 		name       string
 		configText string
+		want       *config.Config
 		wantErr    bool
 	}{
 		{
@@ -264,11 +390,21 @@ func TestLoadSchedulerConfig(t *testing.T) {
 			configText: successWithNoProfileHandlersText,
 			wantErr:    false,
 		},
+		{
+			name:       "errorBadYaml",
+			configText: errorBadYamlText,
+			wantErr:    true,
+		},
 		{
 			name:       "errorBadPluginJson",
 			configText: errorBadPluginJsonText,
 			wantErr:    true,
 		},
+		{
+			name:       "errorNoProfileName",
+			configText: errorNoProfileNameText,
+			wantErr:    true,
+		},
 		{
 			name:       "errorTwoPickers",
 			configText: errorTwoPickersText,
@@ -288,25 +424,17 @@ func TestLoadSchedulerConfig(t *testing.T) {
 
 	registerNeededPlgugins()
 
+	logger := logging.NewTestLogger()
 	for _, test := range tests {
 		handle := utils.NewTestHandle(context.Background())
-		config, err := LoadConfig([]byte(test.configText), handle)
-		if err != nil {
-			if test.wantErr {
-				t.Logf("error was %s", err)
-				continue
-			}
-			t.Fatalf("LoadConfig returned unexpected error: %v", err)
-		}
-
-		_, err = LoadSchedulerConfig(config.SchedulingProfiles, handle)
+		_, err := LoadConfig([]byte(test.configText), handle, logger)
 		if err != nil {
 			if !test.wantErr {
-				t.Errorf("LoadSchedulerConfig returned an unexpected error. error %v", err)
+				t.Errorf("LoadConfig returned an unexpected error. error %v", err)
 			}
 			t.Logf("error was %s", err)
 		} else if test.wantErr {
-			t.Errorf("LoadSchedulerConfig did not return an expected error (%s)", test.name)
+			t.Errorf("LoadConfig did not return an expected error (%s)", test.name)
 		}
 	}
 }
@@ -412,23 +540,6 @@ schedulingProfiles:
   - pluginRef: test1
 `
 
-// missing plugins in scheduling profile
-//
-//nolint:dupword
-const errorNoProfilePluginsText = `
-apiVersion: inference.networking.x-k8s.io/v1alpha1
-kind: EndpointPickerConfig
-plugins:
-- name: test1
-  type: test-one
-  parameters:
-    threshold: 10
-- name: profileHandler
-  type: test-profile-handler
-schedulingProfiles:
-- name: default
-`
-
 // missing required plugin reference name, only weight is provided
 //
 //nolint:dupword
diff --git a/pkg/epp/controller/inferenceobjective_reconciler_test.go b/pkg/epp/controller/inferenceobjective_reconciler_test.go
index 39ce5b4..de43d6e 100644
--- a/pkg/epp/controller/inferenceobjective_reconciler_test.go
+++ b/pkg/epp/controller/inferenceobjective_reconciler_test.go
@@ -43,54 +43,39 @@ var (
 	pool          = utiltest.MakeInferencePool("test-pool1").Namespace("ns1").ObjRef()
 	infObjective1 = utiltest.MakeInferenceObjective("model1").
 			Namespace(pool.Namespace).
-			ModelName("fake model1").
-			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).
-				ModelName(infObjective1.Spec.ModelName).
-				Criticality(*infObjective1.Spec.Criticality).
+				Priority(*infObjective1.Spec.Priority).
 				CreationTimestamp(metav1.Unix(1001, 0)).
-				PoolName("test-pool2").ObjRef()
-	infObjective1NS2 = utiltest.MakeInferenceObjective(infObjective1.Name).
-				Namespace("ns2").
-				ModelName(infObjective1.Spec.ModelName).
-				Criticality(*infObjective1.Spec.Criticality).
-				CreationTimestamp(metav1.Unix(1002, 0)).
-				PoolName(pool.Name).ObjRef()
+				PoolName("test-pool2").
+				PoolGroup("inference.networking.k8s.io").ObjRef()
 	infObjective1Critical = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(infObjective1.Namespace).
-				ModelName(infObjective1.Spec.ModelName).
-				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).
-				ModelName(infObjective1.Spec.ModelName).
 				CreationTimestamp(metav1.Unix(1004, 0)).
 				DeletionTimestamp().
-				PoolName(pool.Name).ObjRef()
-	// Same ModelName, different object with newer creation timestamp
-	infObjective1Newer = utiltest.MakeInferenceObjective("model1-newer").
+				PoolName(pool.Name).
+				PoolGroup("inference.networking.k8s.io").ObjRef()
+	infObjective1DiffGroup = utiltest.MakeInferenceObjective(infObjective1.Name).
 				Namespace(pool.Namespace).
-				ModelName("fake model1").
-				Criticality(v1alpha2.Standard).
+				Priority(1).
 				CreationTimestamp(metav1.Unix(1005, 0)).
-				PoolName(pool.Name).ObjRef()
-	// Same ModelName, different object with older creation timestamp
-	infObjective1Older = utiltest.MakeInferenceObjective("model1-older").
-				Namespace(pool.Namespace).
-				ModelName("fake model1").
-				Criticality(v1alpha2.Standard).
-				CreationTimestamp(metav1.Unix(999, 0)).
-				PoolName(pool.Name).ObjRef()
-
+				PoolName(pool.Name).
+				PoolGroup("inference.networking.x-k8s.io").ObjRef()
 	infObjective2 = utiltest.MakeInferenceObjective("model2").
 			Namespace(pool.Namespace).
-			ModelName("fake model2").
 			CreationTimestamp(metav1.Unix(1000, 0)).
-			PoolName(pool.Name).ObjRef()
+			PoolName(pool.Name).
+			PoolGroup("inference.networking.k8s.io").ObjRef()
 )
 
 func TestInferenceObjectiveReconciler(t *testing.T) {
@@ -127,40 +112,7 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			wantObjectives:     []*v1alpha2.InferenceObjective{},
 		},
 		{
-			name:               "Objective referencing a different pool, different pool name but same namespace",
-			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
-			objective:          infObjective1NS2,
-			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1},
-		},
-		{
-			name:                  "Existing objective changed pools, replaced with another",
-			objectivessInStore:    []*v1alpha2.InferenceObjective{infObjective1},
-			objective:             infObjective1Pool2,
-			objectivesInAPIServer: []*v1alpha2.InferenceObjective{infObjective1Newer},
-			wantObjectives:        []*v1alpha2.InferenceObjective{infObjective1Newer},
-		},
-		{
-			name:                  "Not found, delete existing objective, replaced with another",
-			objectivessInStore:    []*v1alpha2.InferenceObjective{infObjective1},
-			incomingReq:           &types.NamespacedName{Name: infObjective1.Name, Namespace: infObjective1.Namespace},
-			objectivesInAPIServer: []*v1alpha2.InferenceObjective{infObjective1Newer},
-			wantObjectives:        []*v1alpha2.InferenceObjective{infObjective1Newer},
-		},
-		{
-			name:                  "Deletion timestamp set, delete existing objective, replaced with another",
-			objectivessInStore:    []*v1alpha2.InferenceObjective{infObjective1},
-			objective:             infObjective1Deleted,
-			objectivesInAPIServer: []*v1alpha2.InferenceObjective{infObjective1Newer},
-			wantObjectives:        []*v1alpha2.InferenceObjective{infObjective1Newer},
-		},
-		{
-			name:               "Older instance of the objective observed",
-			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
-			objective:          infObjective1Older,
-			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1Older},
-		},
-		{
-			name:               "Objective changed criticality",
+			name:               "Objective changed priority",
 			objectivessInStore: []*v1alpha2.InferenceObjective{infObjective1},
 			objective:          infObjective1Critical,
 			wantObjectives:     []*v1alpha2.InferenceObjective{infObjective1Critical},
@@ -177,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) {
@@ -192,16 +155,14 @@ func TestInferenceObjectiveReconciler(t *testing.T) {
 			for _, m := range test.objectivesInAPIServer {
 				initObjs = append(initObjs, m)
 			}
-
 			fakeClient := fake.NewClientBuilder().
 				WithScheme(scheme).
 				WithObjects(initObjs...).
-				WithIndex(&v1alpha2.InferenceObjective{}, datastore.ModelNameIndexKey, indexInferenceObjectivesByModelName).
 				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.ObjectiveSetIfOlder(m)
+				ds.ObjectiveSet(m)
 			}
 			_ = ds.PoolSet(context.Background(), fakeClient, pool)
 			reconciler := &InferenceObjectiveReconciler{
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
new file mode 100644
index 0000000..2d47de3
--- /dev/null
+++ b/pkg/epp/datalayer/collector_test.go
@@ -0,0 +1,131 @@
+/*
+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"
+	"sync/atomic"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datalayer/mocks"
+)
+
+// --- Test Stubs ---
+
+type DummySource struct {
+	callCount int64
+}
+
+func (d *DummySource) Name() string                   { return "test-dummy-data-source" }
+func (d *DummySource) AddExtractor(_ Extractor) error { return nil }
+func (d *DummySource) Collect(ctx context.Context, ep Endpoint) error {
+	atomic.AddInt64(&d.callCount, 1)
+	return nil
+}
+
+func defaultEndpoint() Endpoint {
+	ms := NewEndpoint()
+	pod := &corev1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "pod-name",
+			Namespace: "default",
+		},
+		Status: corev1.PodStatus{
+			PodIP: "1.2.3.4",
+		},
+	}
+	ms.UpdatePod(pod)
+	return ms
+}
+
+// --- Tests ---
+
+var (
+	endpoint = defaultEndpoint()
+	sources  = []DataSource{&DummySource{}}
+)
+
+func TestCollectorCanStartOnlyOnce(t *testing.T) {
+	c := NewCollector()
+	ctx := context.Background()
+	ticker := mocks.NewTicker()
+
+	err := c.Start(ctx, ticker, endpoint, sources)
+	require.NoError(t, err, "first Start call should succeed")
+
+	err = c.Start(ctx, ticker, endpoint, sources)
+	assert.Error(t, err, "multiple collector start should error")
+}
+
+func TestCollectorStopBeforeStartIsAnError(t *testing.T) {
+	c := NewCollector()
+	err := c.Stop()
+	assert.Error(t, err, "collector stop called before start should error")
+}
+
+func TestCollectorCanStopOnlyOnce(t *testing.T) {
+	c := NewCollector()
+	ctx := context.Background()
+	ticker := mocks.NewTicker()
+
+	require.NoError(t, c.Start(ctx, ticker, endpoint, sources))
+	require.NoError(t, c.Stop(), "first Stop should succeed")
+	assert.Error(t, c.Stop(), "second Stop should fail")
+}
+
+func TestCollectorCollectsOnTicks(t *testing.T) {
+	source := &DummySource{}
+	c := NewCollector()
+	ticker := mocks.NewTicker()
+	ctx := context.Background()
+
+	require.NoError(t, c.Start(ctx, ticker, endpoint, []DataSource{source}))
+	ticker.Tick()
+	ticker.Tick()
+
+	// 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())
+}
+
+func TestCollectorStopCancelsContext(t *testing.T) {
+	source := &DummySource{}
+	c := NewCollector()
+	ticker := mocks.NewTicker()
+	ctx := context.Background()
+
+	require.NoError(t, c.Start(ctx, ticker, endpoint, []DataSource{source}))
+	ticker.Tick() // should be processed
+	time.Sleep(20 * time.Millisecond)
+
+	require.NoError(t, c.Stop())
+	before := atomic.LoadInt64(&source.callCount)
+
+	ticker.Tick()
+	time.Sleep(20 * time.Millisecond) // let collector run again
+	after := atomic.LoadInt64(&source.callCount)
+	assert.Equal(t, before, after, "call count changed after stop")
+}
diff --git a/pkg/epp/datalayer/datasource_test.go b/pkg/epp/datalayer/datasource_test.go
index c5e7cd5..90919cc 100644
--- a/pkg/epp/datalayer/datasource_test.go
+++ b/pkg/epp/datalayer/datasource_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package datalayer
 
 import (
+	"context"
 	"reflect"
 	"testing"
 
@@ -27,9 +28,9 @@ type mockDataSource struct {
 	name string
 }
 
-func (m *mockDataSource) Name() string                   { return m.name }
-func (m *mockDataSource) AddExtractor(_ Extractor) error { return nil }
-func (m *mockDataSource) Collect(_ Endpoint)             {}
+func (m *mockDataSource) Name() string                                { return m.name }
+func (m *mockDataSource) AddExtractor(_ Extractor) error              { return nil }
+func (m *mockDataSource) Collect(_ context.Context, _ Endpoint) error { return nil }
 
 func TestRegisterAndGetSource(t *testing.T) {
 	reg := DataSourceRegistry{}
diff --git a/pkg/epp/datalayer/metrics/spec_test.go b/pkg/epp/datalayer/metrics/spec_test.go
new file mode 100644
index 0000000..b4ad285
--- /dev/null
+++ b/pkg/epp/datalayer/metrics/spec_test.go
@@ -0,0 +1,479 @@
+/*
+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 metrics
+
+import (
+	"errors"
+	"strconv"
+	"strings"
+	"testing"
+
+	dto "github.com/prometheus/client_model/go"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/protobuf/proto"
+)
+
+// --- Test Helpers ---
+
+func makeMetric(labels map[string]string, value float64, timestampMs int64) *dto.Metric {
+	labelPairs := []*dto.LabelPair{}
+	for k, v := range labels {
+		labelPairs = append(labelPairs, &dto.LabelPair{Name: proto.String(k), Value: proto.String(v)})
+	}
+	return &dto.Metric{
+		Label:       labelPairs,
+		Gauge:       &dto.Gauge{Value: &value},
+		TimestampMs: &timestampMs,
+	}
+}
+
+func makeMetricFamily(name string, metrics ...*dto.Metric) *dto.MetricFamily {
+	return &dto.MetricFamily{
+		Name:   &name,
+		Type:   dto.MetricType_GAUGE.Enum(),
+		Metric: metrics,
+	}
+}
+
+// TestStringToMetricSpec determines parsing of metric's specification.
+func TestStringToMetricSpec(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   string
+		want    *Spec
+		wantErr bool
+	}{
+		{
+			name:    "empty string",
+			input:   "",
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name:  "no labels",
+			input: "my_metric",
+			want: &Spec{
+				Name:   "my_metric",
+				Labels: map[string]string{},
+			},
+			wantErr: false,
+		},
+		{
+			name:  "one label",
+			input: "my_metric{label1=value1}",
+			want: &Spec{
+				Name: "my_metric",
+				Labels: map[string]string{
+					"label1": "value1",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:  "label with quoted value",
+			input: "my_metric{label1=\"value1\"}",
+			want: &Spec{
+				Name: "my_metric",
+				Labels: map[string]string{
+					"label1": "value1",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:  "multiple labels",
+			input: "my_metric{label1=value1,label2=value2}",
+			want: &Spec{
+				Name: "my_metric",
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:  "extra whitespace",
+			input: "  my_metric  {  label1  =  value1  ,  label2  =  value2  }  ",
+			want: &Spec{
+				Name: "my_metric",
+				Labels: map[string]string{
+					"label1": "value1",
+					"label2": "value2",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name:    "missing closing brace",
+			input:   "my_metric{label1=value1",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "missing opening brace",
+			input:   "my_metriclabel1=value1}",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "invalid label pair",
+			input:   "my_metric{label1}",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty label name",
+			input:   "my_metric{=value1}",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty label value",
+			input:   "my_metric{label1=}",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty label name and value with spaces",
+			input:   "my_metric{  =  }",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "characters after closing brace",
+			input:   "my_metric{label=val}extra",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:    "empty metric name",
+			input:   "{label=val}",
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name:  "no labels and just metric name with space",
+			input: "my_metric ",
+			want: &Spec{
+				Name:   "my_metric",
+				Labels: map[string]string{},
+			},
+			wantErr: false,
+		},
+		{
+			name:  "no labels and just metric name with space before and after",
+			input: "  my_metric  ",
+			want: &Spec{
+				Name:   "my_metric",
+				Labels: map[string]string{},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseStringToSpec(tt.input)
+
+			if tt.wantErr {
+				assert.Error(t, err, "expected error but got none")
+			} else {
+				assert.NoError(t, err, "unexpected error from parseStringToSpec")
+				if tt.want != nil && got != nil {
+					assert.Equal(t, tt.want, got)
+				} else {
+					assert.Equal(t, tt.want, got, "expected and actual values don't match")
+				}
+			}
+		})
+	}
+}
+
+// TestGetMetric checks retrieving of standard metrics from a metric families.
+func TestGetMetric(t *testing.T) {
+	metricFamilies := PrometheusMetricMap{
+		"metric1": makeMetricFamily("metric1",
+			makeMetric(map[string]string{"label1": "value1"}, 1.0, 1000),
+			makeMetric(map[string]string{"label1": "value2"}, 2.0, 2000),
+		),
+		"metric2": makeMetricFamily("metric2",
+			makeMetric(map[string]string{"labelA": "A1", "labelB": "B1"}, 3.0, 1500),
+			makeMetric(map[string]string{"labelA": "A2", "labelB": "B2"}, 4.0, 2500),
+		),
+		"metric3": makeMetricFamily("metric3",
+			makeMetric(map[string]string{}, 5.0, 3000),
+			makeMetric(map[string]string{}, 6.0, 1000),
+		),
+	}
+
+	tests := []struct {
+		name      string
+		spec      Spec
+		expected  float64
+		wantError bool
+	}{
+		{
+			name: "get labeled metric, exists",
+			spec: Spec{
+				Name:   "metric1",
+				Labels: map[string]string{"label1": "value1"},
+			},
+			expected:  1.0,
+			wantError: false,
+		},
+		{
+			name: "get labeled metric, wrong value",
+			spec: Spec{
+				Name:   "metric1",
+				Labels: map[string]string{"label1": "value3"},
+			},
+			expected:  -1, // Expect an error, not a specific value
+			wantError: true,
+		},
+		{
+			name: "get labeled metric, missing label",
+			spec: Spec{
+				Name:   "metric1",
+				Labels: map[string]string{"label2": "value2"},
+			},
+			expected:  -1,
+			wantError: true,
+		},
+		{
+			name: "get labeled metric, extra label present",
+			spec: Spec{
+				Name:   "metric2",
+				Labels: map[string]string{"labelA": "A1"},
+			},
+			expected:  3.0,
+			wantError: false,
+		},
+		{
+			name: "get unlabeled metric, exists",
+			spec: Spec{
+				Name:   "metric3",
+				Labels: nil, // Explicitly nil
+			},
+			expected:  5.0, // latest metric, which occurs first in our test data
+			wantError: false,
+		},
+		{
+			name: "get unlabeled metric, metric family not found",
+			spec: Spec{
+				Name:   "metric4",
+				Labels: nil,
+			},
+			expected:  -1,
+			wantError: true,
+		},
+		{
+			name: "get labeled metric, metric family not found",
+			spec: Spec{
+				Name:   "metric4",
+				Labels: map[string]string{"label1": "value1"},
+			},
+			expected:  -1,
+			wantError: true,
+		},
+		{
+			name: "get metric, no metrics available",
+			spec: Spec{
+				Name: "empty_metric",
+			},
+			expected:  -1,
+			wantError: true,
+		},
+		{
+			name: "get latest metric",
+			spec: Spec{
+				Name:   "metric3",
+				Labels: map[string]string{}, // Empty map, not nil
+			},
+			expected:  5.0,
+			wantError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			metric, err := tt.spec.getLatestMetric(metricFamilies)
+			if tt.wantError {
+				assert.Error(t, err, "expected error but got nil")
+			} else {
+				require.NoError(t, err, "unexpected error from getLatestMetric")
+				value := extractValue(metric)
+				assert.Equal(t, tt.expected, value, "metric value mismatch")
+			}
+		})
+	}
+}
+
+func TestGetLoRAMetric(t *testing.T) {
+	loraSpec := &LoRASpec{
+		Spec: &Spec{
+			Name: "vllm:lora_requests_info",
+		},
+	}
+
+	testCases := []struct {
+		name             string
+		metricFamilies   PrometheusMetricMap
+		expectedAdapters map[string]int
+		expectedMax      int
+		expectedErr      error
+		spec             *LoRASpec
+	}{
+		{
+			name: "no lora metrics",
+			metricFamilies: PrometheusMetricMap{
+				"some_other_metric": makeMetricFamily("some_other_metric",
+					makeMetric(nil, 1.0, 1000),
+				),
+			},
+			expectedAdapters: map[string]int{},
+			expectedMax:      0,
+			expectedErr:      errors.New("metric family \"vllm:lora_requests_info\" not found"), // Expect an error because the family is missing
+			spec:             loraSpec,
+		},
+		{
+			name: "basic lora metrics",
+			metricFamilies: PrometheusMetricMap{
+				"vllm:lora_requests_info": makeMetricFamily("vllm:lora_requests_info",
+					makeMetric(map[string]string{"running_lora_adapters": "lora1", "max_lora": "2"}, 3000.0, 1000),       // Newer
+					makeMetric(map[string]string{"running_lora_adapters": "lora2,lora3", "max_lora": "4"}, 1000.0, 1000), // Older
+
+				),
+			},
+			expectedAdapters: map[string]int{"lora1": 0},
+			expectedMax:      2,
+			expectedErr:      nil,
+			spec:             loraSpec,
+		},
+		{
+			name: "no matching lora metrics",
+			metricFamilies: PrometheusMetricMap{
+				"vllm:lora_requests_info": makeMetricFamily("vllm:lora_requests_info",
+					makeMetric(map[string]string{"other_label": "value"}, 5.0, 3000),
+				),
+			},
+			expectedAdapters: map[string]int{},
+			expectedMax:      0,
+			expectedErr:      nil, // Expect *no* error; just no adapters found
+			spec:             loraSpec,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			metric, err := tc.spec.getLatestMetric(tc.metricFamilies)
+
+			if tc.expectedErr != nil {
+				assert.Error(t, err)
+				assert.EqualError(t, err, tc.expectedErr.Error())
+				return
+			}
+
+			require.NoError(t, err)
+
+			if tc.spec == nil {
+				assert.Nil(t, metric)
+				return
+			}
+
+			if tc.expectedAdapters == nil && metric == nil {
+				return
+			}
+
+			require.NotNil(t, metric, "expected non-nil metric")
+
+			adaptersFound := make(map[string]int)
+			maxLora := 0
+
+			for _, label := range metric.GetLabel() {
+				switch label.GetName() {
+				case "running_lora_adapters", "waiting_lora_adapters":
+					if label.GetValue() != "" {
+						for _, adapter := range strings.Split(label.GetValue(), ",") {
+							adaptersFound[adapter] = 0
+						}
+					}
+				case "max_lora":
+					parsed, parseErr := strconv.Atoi(label.GetValue())
+					require.NoError(t, parseErr, "failed to parse max_lora")
+					maxLora = parsed
+				}
+			}
+
+			assert.Equal(t, tc.expectedAdapters, adaptersFound, "adapters mismatch")
+			assert.Equal(t, tc.expectedMax, maxLora, "maxLora mismatch")
+		})
+	}
+}
+
+func TestLabelsMatch(t *testing.T) {
+	tests := []struct {
+		name         string
+		metricLabels []*dto.LabelPair
+		spec         *Spec
+		want         bool
+	}{
+		{
+			name:         "empty spec labels, should match",
+			metricLabels: []*dto.LabelPair{{Name: proto.String("a"), Value: proto.String("b")}},
+			spec:         &Spec{Labels: map[string]string{}},
+			want:         true,
+		},
+		{
+			name:         "nil spec labels, should match",
+			metricLabels: []*dto.LabelPair{{Name: proto.String("a"), Value: proto.String("b")}},
+			spec:         &Spec{},
+			want:         true,
+		},
+		{
+			name:         "exact match",
+			metricLabels: []*dto.LabelPair{{Name: proto.String("a"), Value: proto.String("b")}},
+			spec:         &Spec{Labels: map[string]string{"a": "b"}},
+			want:         true,
+		},
+		{
+			name:         "extra labels in metric",
+			metricLabels: []*dto.LabelPair{{Name: proto.String("a"), Value: proto.String("b")}, {Name: proto.String("c"), Value: proto.String("d")}},
+			spec:         &Spec{Labels: map[string]string{"a": "b"}},
+			want:         true,
+		},
+		{
+			name:         "missing label in metric",
+			metricLabels: []*dto.LabelPair{{Name: proto.String("a"), Value: proto.String("b")}},
+			spec:         &Spec{Labels: map[string]string{"a": "b", "c": "d"}},
+			want:         false,
+		},
+		{
+			name:         "value mismatch",
+			metricLabels: []*dto.LabelPair{{Name: proto.String("a"), Value: proto.String("b")}},
+			spec:         &Spec{Labels: map[string]string{"a": "c"}},
+			want:         false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.spec.labelsMatch(tt.metricLabels)
+			assert.Equal(t, tt.want, got, "labelsMatch() mismatch")
+		})
+	}
+}
diff --git a/pkg/epp/datastore/datastore_test.go b/pkg/epp/datastore/datastore_test.go
index 72ea3f3..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()
@@ -106,28 +106,17 @@ func TestPool(t *testing.T) {
 	}
 }
 
-func TestModel(t *testing.T) {
+func TestObjective(t *testing.T) {
 	chatModel := "chat"
 	tsModel := "food-review"
-	model1ts := testutil.MakeInferenceObjective("model1").
-		CreationTimestamp(metav1.Unix(1000, 0)).
-		ModelName(tsModel).ObjRef()
+	model1ts := testutil.MakeInferenceObjective("model1").ObjRef()
 	// Same model name as model1ts, different object name.
-	model2ts := testutil.MakeInferenceObjective("model2").
-		CreationTimestamp(metav1.Unix(1001, 0)).
-		ModelName(tsModel).ObjRef()
+	model2ts := testutil.MakeInferenceObjective("model2").ObjRef()
 	// Same model name as model1ts, newer timestamp
-	model1tsNewer := testutil.MakeInferenceObjective("model1").
-		CreationTimestamp(metav1.Unix(1002, 0)).
-		Criticality(v1alpha2.Critical).
-		ModelName(tsModel).ObjRef()
-	model2tsNewer := testutil.MakeInferenceObjective("model2").
-		CreationTimestamp(metav1.Unix(1003, 0)).
-		ModelName(tsModel).ObjRef()
+	model1tsCritical := testutil.MakeInferenceObjective("model1").
+		Priority(2).ObjRef()
 	// Same object name as model2ts, different model name.
-	model2chat := testutil.MakeInferenceObjective(model2ts.Name).
-		CreationTimestamp(metav1.Unix(1005, 0)).
-		ModelName(chatModel).ObjRef()
+	model2chat := testutil.MakeInferenceObjective(model2ts.Name).ObjRef()
 
 	tests := []struct {
 		name           string
@@ -139,43 +128,28 @@ func TestModel(t *testing.T) {
 		{
 			name: "Add model1 with food-review as modelName",
 			op: func(ds Datastore) bool {
-				return ds.ObjectiveSetIfOlder(model1ts)
+				ds.ObjectiveSet(model1ts)
+				return cmp.Diff(ds.ObjectiveGet(model1ts.Name), model1ts) == ""
 			},
 			wantModels:   []*v1alpha2.InferenceObjective{model1ts},
 			wantOpResult: true,
 		},
 		{
-			name:           "Set model1 with the same modelName, but with diff criticality and newer creation timestamp, should update.",
+			name:           "Set model1 with the same modelName, but with diff priority, should update.",
 			existingModels: []*v1alpha2.InferenceObjective{model1ts},
 			op: func(ds Datastore) bool {
-				return ds.ObjectiveSetIfOlder(model1tsNewer)
+				ds.ObjectiveSet(model1tsCritical)
+				return cmp.Diff(ds.ObjectiveGet(model1tsCritical.Name), model1tsCritical) == ""
 			},
 			wantOpResult: true,
-			wantModels:   []*v1alpha2.InferenceObjective{model1tsNewer},
-		},
-		{
-			name:           "set model2 with the same modelName, but newer creation timestamp, should not update.",
-			existingModels: []*v1alpha2.InferenceObjective{model1tsNewer},
-			op: func(ds Datastore) bool {
-				return ds.ObjectiveSetIfOlder(model2tsNewer)
-			},
-			wantOpResult: false,
-			wantModels:   []*v1alpha2.InferenceObjective{model1tsNewer},
-		},
-		{
-			name:           "Set model2 with the same modelName, but older creation timestamp, should update",
-			existingModels: []*v1alpha2.InferenceObjective{model1tsNewer},
-			op: func(ds Datastore) bool {
-				return ds.ObjectiveSetIfOlder(model2ts)
-			},
-			wantOpResult: true,
-			wantModels:   []*v1alpha2.InferenceObjective{model2ts},
+			wantModels:   []*v1alpha2.InferenceObjective{model1tsCritical},
 		},
 		{
 			name:           "Set model1 with the food-review modelName, both models should exist",
 			existingModels: []*v1alpha2.InferenceObjective{model2chat},
 			op: func(ds Datastore) bool {
-				return ds.ObjectiveSetIfOlder(model1ts)
+				ds.ObjectiveSet(model1ts)
+				return cmp.Diff(ds.ObjectiveGet(model1ts.Name), model1ts) == ""
 			},
 			wantOpResult: true,
 			wantModels:   []*v1alpha2.InferenceObjective{model2chat, model1ts},
@@ -184,7 +158,8 @@ func TestModel(t *testing.T) {
 			name:           "Set model1 with the food-review modelName, both models should exist",
 			existingModels: []*v1alpha2.InferenceObjective{model2chat, model1ts},
 			op: func(ds Datastore) bool {
-				return ds.ObjectiveSetIfOlder(model1ts)
+				ds.ObjectiveSet(model1ts)
+				return cmp.Diff(ds.ObjectiveGet(model1ts.Name), model1ts) == ""
 			},
 			wantOpResult: true,
 			wantModels:   []*v1alpha2.InferenceObjective{model2chat, model1ts},
@@ -196,16 +171,16 @@ func TestModel(t *testing.T) {
 				gotChat := ds.ObjectiveGet(chatModel)
 				return gotChat != nil && cmp.Diff(model2chat, gotChat) == ""
 			},
-			wantOpResult: true,
+			wantOpResult: false,
 			wantModels:   []*v1alpha2.InferenceObjective{model2chat, model1ts},
 		},
 		{
 			name:           "Delete the model",
 			existingModels: []*v1alpha2.InferenceObjective{model2chat, model1ts},
 			op: func(ds Datastore) bool {
-				existing := ds.ObjectiveDelete(types.NamespacedName{Name: model1ts.Name, Namespace: model1ts.Namespace})
+				ds.ObjectiveDelete(types.NamespacedName{Name: model1ts.Name, Namespace: model1ts.Namespace})
 				got := ds.ObjectiveGet(tsModel)
-				return existing != nil && got == nil
+				return got == nil
 
 			},
 			wantOpResult: true,
@@ -214,10 +189,10 @@ func TestModel(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.ObjectiveSetIfOlder(m)
+				ds.ObjectiveSet(m)
 			}
 
 			gotOpResult := test.op(ds)
@@ -270,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))}},
 		},
 	}
 )
@@ -281,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",
@@ -338,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())
@@ -432,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)
@@ -440,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/handlers/request_test.go b/pkg/epp/handlers/request_test.go
index 796082c..4ae2078 100644
--- a/pkg/epp/handlers/request_test.go
+++ b/pkg/epp/handlers/request_test.go
@@ -17,20 +17,18 @@ limitations under the License.
 package handlers
 
 import (
-	"context"
 	"testing"
 
 	configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 )
 
 func TestHandleRequestHeaders(t *testing.T) {
 	t.Parallel()
 
 	// Setup a mock server and request context
-	server := &StreamingServer{
-		fairnessIDHeaderKey: "test-fairness-id",
-	}
+	server := &StreamingServer{}
 
 	reqCtx := &RequestContext{
 		Request: &Request{
@@ -47,7 +45,7 @@ func TestHandleRequestHeaders(t *testing.T) {
 						Value: "test-value",
 					},
 					{
-						Key:   "test-fairness-id",
+						Key:   metadata.FlowFairnessIDKey,
 						Value: "test-fairness-id-value",
 					},
 				},
@@ -56,7 +54,7 @@ func TestHandleRequestHeaders(t *testing.T) {
 		},
 	}
 
-	err := server.HandleRequestHeaders(context.Background(), reqCtx, req)
+	err := server.HandleRequestHeaders(reqCtx, req)
 	if err != nil {
 		t.Fatalf("expected no error, got %v", err)
 	}
@@ -64,7 +62,7 @@ func TestHandleRequestHeaders(t *testing.T) {
 	if reqCtx.FairnessID != "test-fairness-id-value" {
 		t.Errorf("expected fairness ID to be 'test-fairness-id-value', got %s", reqCtx.FairnessID)
 	}
-	if reqCtx.Request.Headers["test-fairness-id"] == "test-fairness-id-value" {
+	if reqCtx.Request.Headers[metadata.FlowFairnessIDKey] == "test-fairness-id-value" {
 		t.Errorf("expected fairness ID header to be removed from request headers, but it was not")
 	}
 }
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 b0e5aa1..e7968d4 100644
--- a/pkg/epp/requestcontrol/director_test.go
+++ b/pkg/epp/requestcontrol/director_test.go
@@ -34,11 +34,11 @@ 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"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/plugins"
 	schedulingtypes "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
 	errutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/error"
@@ -74,37 +74,38 @@ func TestDirector_HandleRequest(t *testing.T) {
 	modelSheddable := "food-review-sheddable"
 	modelWithResolvedTarget := "food-review-resolve"
 
+	objectiveName := "ioFoodReview"
+	objectiveNameSheddable := "imFoodReviewSheddable"
+	objectiveNameResolve := "imFoodReviewResolve"
 	// InferenceObjective definitions
-	imFoodReview := testutil.MakeInferenceObjective("imFoodReview").
+	ioFoodReview := testutil.MakeInferenceObjective("ioFoodReview").
 		CreationTimestamp(metav1.Unix(1000, 0)).
-		ModelName(model).
-		Criticality(v1alpha2.Critical).
+		Priority(2).
 		ObjRef()
-	imFoodReviewSheddable := testutil.MakeInferenceObjective("imFoodReviewSheddable").
+	ioFoodReviewSheddable := testutil.MakeInferenceObjective("imFoodReviewSheddable").
 		CreationTimestamp(metav1.Unix(1000, 0)).
-		ModelName(modelSheddable).
-		Criticality(v1alpha2.Sheddable).
+		Priority(-1).
 		ObjRef()
-	imFoodReviewResolve := testutil.MakeInferenceObjective("imFoodReviewResolve").
+	ioFoodReviewResolve := testutil.MakeInferenceObjective("imFoodReviewResolve").
 		CreationTimestamp(metav1.Unix(1000, 0)).
-		ModelName(modelWithResolvedTarget).
-		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.ObjectiveSetIfOlder(imFoodReview)
-	ds.ObjectiveSetIfOlder(imFoodReviewResolve)
-	ds.ObjectiveSetIfOlder(imFoodReviewSheddable)
+	ds.ObjectiveSet(ioFoodReview)
+	ds.ObjectiveSet(ioFoodReviewResolve)
+	ds.ObjectiveSet(ioFoodReviewSheddable)
 
 	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",
+				},
 			},
 		},
 	}
@@ -171,10 +172,12 @@ func TestDirector_HandleRequest(t *testing.T) {
 		name                   string
 		reqBodyMap             map[string]any
 		mockSaturationDetector *mockSaturationDetector
+		inferenceObjectiveName string
 		schedulerMockSetup     func(m *mockScheduler)
 		wantErrCode            string                   // Expected errutil code string
 		wantReqCtx             *handlers.RequestContext // Fields to check in the returned RequestContext
 		wantMutatedBodyModel   string                   // Expected model in reqCtx.Request.Body after PostDispatch
+		targetModelName        string                   // Expected model name after target model resolution
 	}{
 		{
 			name: "successful completions request (critical, saturation ignored)",
@@ -187,18 +190,20 @@ func TestDirector_HandleRequest(t *testing.T) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				Model:               model,
-				ResolvedTargetModel: model,
+				ObjectiveKey:    objectiveName,
+				TargetModelName: model,
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
 					Address:        "192.168.1.100",
 				},
 				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
 			},
-			wantMutatedBodyModel: model,
+			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{
@@ -208,12 +213,12 @@ func TestDirector_HandleRequest(t *testing.T) {
 					},
 				},
 			},
+			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				Model:               model,
-				ResolvedTargetModel: model,
+				TargetModelName: model,
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
 					Address:        "192.168.1.100",
@@ -221,6 +226,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
 			},
 			wantMutatedBodyModel: model,
+			targetModelName:      model,
 		},
 		{
 			name: "successful chat completions request with multiple messages (critical, saturation ignored)",
@@ -241,15 +247,17 @@ func TestDirector_HandleRequest(t *testing.T) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				Model:               model,
-				ResolvedTargetModel: model,
+				ObjectiveKey:    objectiveName,
+				TargetModelName: model,
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
 					Address:        "192.168.1.100",
 				},
 				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
 			},
-			wantMutatedBodyModel: model,
+			wantMutatedBodyModel:   model,
+			inferenceObjectiveName: objectiveName,
+			targetModelName:        model,
 		},
 		{
 			name: "successful completions request (sheddable, not saturated)",
@@ -262,15 +270,17 @@ func TestDirector_HandleRequest(t *testing.T) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				Model:               modelSheddable,
-				ResolvedTargetModel: modelSheddable,
+				ObjectiveKey:    objectiveNameSheddable,
+				TargetModelName: modelSheddable,
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
 					Address:        "192.168.1.100",
 				},
 				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
 			},
-			wantMutatedBodyModel: modelSheddable,
+			wantMutatedBodyModel:   modelSheddable,
+			inferenceObjectiveName: objectiveNameSheddable,
+			targetModelName:        modelSheddable,
 		},
 		{
 			name: "successful request with target model resolution",
@@ -283,15 +293,17 @@ func TestDirector_HandleRequest(t *testing.T) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				Model:               modelWithResolvedTarget,
-				ResolvedTargetModel: "resolved-target-model-A",
+				ObjectiveKey:    objectiveNameResolve,
+				TargetModelName: "resolved-target-model-A",
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
 					Address:        "192.168.1.100",
 				},
 				TargetEndpoint: "192.168.1.100:8000,192.168.2.100:8000,192.168.4.100:8000",
 			},
-			wantMutatedBodyModel: "resolved-target-model-A",
+			wantMutatedBodyModel:   "resolved-target-model-A",
+			inferenceObjectiveName: objectiveNameResolve,
+			targetModelName:        "resolved-target-model-A",
 		},
 		{
 			name: "nonexistent target defined, use default inference model",
@@ -299,8 +311,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 				m.scheduleResults = defaultSuccessfulScheduleResults
 			},
 			wantReqCtx: &handlers.RequestContext{
-				Model:               "food-review-1",
-				ResolvedTargetModel: "food-review-1",
+				ObjectiveKey:    "food-review-1",
+				TargetModelName: "food-review-1",
 				TargetPod: &backend.Pod{
 					NamespacedName: types.NamespacedName{Namespace: "default", Name: "pod1"},
 					Address:        "192.168.1.100",
@@ -313,6 +325,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 				"prompt": "test prompt",
 			},
 			mockSaturationDetector: &mockSaturationDetector{isSaturated: false},
+			inferenceObjectiveName: "food-review-1",
+			targetModelName:        "food-review-1",
 		},
 		{
 
@@ -321,6 +335,7 @@ func TestDirector_HandleRequest(t *testing.T) {
 				"model":  modelSheddable,
 				"prompt": "sheddable prompt",
 			},
+			inferenceObjectiveName: objectiveNameSheddable,
 			mockSaturationDetector: &mockSaturationDetector{isSaturated: true},
 			wantErrCode:            errutil.InferencePoolResourceExhausted,
 		},
@@ -353,7 +368,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 			schedulerMockSetup: func(m *mockScheduler) {
 				m.scheduleErr = errors.New("simulated scheduler failure")
 			},
-			wantErrCode: errutil.InferencePoolResourceExhausted,
+			wantErrCode:            errutil.InferencePoolResourceExhausted,
+			inferenceObjectiveName: objectiveName,
 		},
 		{
 			name: "scheduler returns nil result and nil error",
@@ -365,7 +381,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 				m.scheduleResults = nil
 				m.scheduleErr = nil
 			},
-			wantErrCode: errutil.Internal,
+			wantErrCode:            errutil.Internal,
+			inferenceObjectiveName: objectiveName,
 		},
 	}
 
@@ -385,6 +402,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 						requtil.RequestIdHeaderKey: "test-req-id-" + test.name, // Ensure a default request ID
 					},
 				},
+				ObjectiveKey:    test.inferenceObjectiveName,
+				TargetModelName: test.targetModelName,
 			}
 			// Deep copy the body map.
 			for k, v := range test.reqBodyMap {
@@ -405,8 +424,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 			assert.NoError(t, err, "HandleRequest() returned unexpected error")
 
 			if test.wantReqCtx != nil {
-				assert.Equal(t, test.wantReqCtx.Model, returnedReqCtx.Model, "reqCtx.Model mismatch")
-				assert.Equal(t, test.wantReqCtx.ResolvedTargetModel, returnedReqCtx.ResolvedTargetModel,
+				assert.Equal(t, test.wantReqCtx.ObjectiveKey, returnedReqCtx.ObjectiveKey, "reqCtx.Model mismatch")
+				assert.Equal(t, test.wantReqCtx.TargetModelName, returnedReqCtx.TargetModelName,
 					"reqCtx.ResolvedTargetModel mismatch")
 				assert.Equal(t, test.wantReqCtx.TargetPod, returnedReqCtx.TargetPod, "reqCtx.TargetPod mismatch")
 				assert.Equal(t, test.wantReqCtx.TargetEndpoint, returnedReqCtx.TargetEndpoint, "reqCtx.TargetEndpoint mismatch")
@@ -425,8 +444,8 @@ func TestDirector_HandleRequest(t *testing.T) {
 func TestGetCandidatePodsForScheduling(t *testing.T) {
 	var makeFilterMetadata = func(data []any) map[string]any {
 		return map[string]any{
-			"envoy.lb.subset_hint": map[string]any{
-				"x-gateway-destination-endpoint-subset": data,
+			metadata.SubsetFilterNamespace: map[string]any{
+				metadata.SubsetFilterKey: data,
 			},
 		}
 	}
@@ -483,7 +502,7 @@ func TestGetCandidatePodsForScheduling(t *testing.T) {
 		},
 		{
 			name:     "SubsetFilter, namespace present filter not present — return all pods",
-			metadata: map[string]any{"envoy.lb.subset_hint": map[string]any{}},
+			metadata: map[string]any{metadata.SubsetFilterNamespace: map[string]any{}},
 			output: []schedulingtypes.Pod{
 				&schedulingtypes.PodMetrics{
 					Pod:          outputPod1,
@@ -531,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)
@@ -553,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
@@ -654,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)
@@ -672,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/server/server_test.go b/pkg/epp/server/server_test.go
index fc075de..dfcc4bb 100644
--- a/pkg/epp/server/server_test.go
+++ b/pkg/epp/server/server_test.go
@@ -28,40 +28,37 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/apix/v1alpha2"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/handlers"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	testutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/testing"
 	"sigs.k8s.io/gateway-api-inference-extension/test/utils"
 )
 
 const (
-	bufSize                    = 1024 * 1024
-	podName                    = "pod1"
-	podAddress                 = "1.2.3.4"
-	poolPort                   = int32(5678)
-	destinationEndpointHintKey = "test-target"
-	fairnessIDHeaderKey        = "x-fairness-id"
-	namespace                  = "ns1"
+	bufSize    = 1024 * 1024
+	podName    = "pod1"
+	podAddress = "1.2.3.4"
+	poolPort   = int32(5678)
+	namespace  = "ns1"
 )
 
 func TestServer(t *testing.T) {
 	theHeaderValue := "body"
 	requestHeader := "x-test"
 
-	expectedRequestHeaders := map[string]string{destinationEndpointHintKey: fmt.Sprintf("%s:%d", podAddress, poolPort),
+	expectedRequestHeaders := map[string]string{metadata.DestinationEndpointKey: fmt.Sprintf("%s:%d", podAddress, poolPort),
 		"Content-Length": "42", ":method": "POST", requestHeader: theHeaderValue}
 	expectedResponseHeaders := map[string]string{"x-went-into-resp-headers": "true", ":method": "POST", requestHeader: theHeaderValue}
 	expectedSchedulerHeaders := map[string]string{":method": "POST", requestHeader: theHeaderValue}
 
 	t.Run("server", func(t *testing.T) {
-		tsModel := "food-review"
 		model := testutil.MakeInferenceObjective("v1").
-			CreationTimestamp(metav1.Unix(1000, 0)).
-			ModelName(tsModel).ObjRef()
+			CreationTimestamp(metav1.Unix(1000, 0)).ObjRef()
 
 		director := &testDirector{}
 		ctx, cancel, ds, _ := utils.PrepareForTestStreamingServer([]*v1alpha2.InferenceObjective{model},
 			[]*v1.Pod{{ObjectMeta: metav1.ObjectMeta{Name: podName}}}, "test-pool1", namespace, poolPort)
 
-		streamingServer := handlers.NewStreamingServer(namespace, destinationEndpointHintKey, fairnessIDHeaderKey, ds, director)
+		streamingServer := handlers.NewStreamingServer(ds, director)
 
 		testListener, errChan := utils.SetupTestStreamingServer(t, ctx, ds, streamingServer)
 		process, conn := utils.GetStreamingServerClient(ctx, t)
@@ -69,9 +66,9 @@ func TestServer(t *testing.T) {
 
 		// Send request headers - no response expected
 		headers := utils.BuildEnvoyGRPCHeaders(map[string]string{
-			requestHeader:       theHeaderValue,
-			":method":           "POST",
-			fairnessIDHeaderKey: "a-very-interesting-fairness-id",
+			requestHeader:              theHeaderValue,
+			":method":                  "POST",
+			metadata.FlowFairnessIDKey: "a-very-interesting-fairness-id",
 		}, true)
 		request := &pb.ProcessingRequest{
 			Request: &pb.ProcessingRequest_RequestHeaders{
diff --git a/pkg/epp/util/testing/wrappers.go b/pkg/epp/util/testing/wrappers.go
index 84a6015..9eeb3ad 100644
--- a/pkg/epp/util/testing/wrappers.go
+++ b/pkg/epp/util/testing/wrappers.go
@@ -136,23 +136,18 @@ func (m *InferenceObjectiveWrapper) ObjRef() *v1alpha2.InferenceObjective {
 	return &m.InferenceObjective
 }
 
-func (m *InferenceObjectiveWrapper) ModelName(modelName string) *InferenceObjectiveWrapper {
-	m.Spec.ModelName = modelName
-	return m
-}
-
-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
 }
 
@@ -180,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{},
 		},
 	}
@@ -195,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
 }
 
@@ -214,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,
@@ -231,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)
@@ -245,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 bea0c1b..e01240d 100644
--- a/test/e2e/epp/e2e_test.go
+++ b/test/e2e/epp/e2e_test.go
@@ -28,13 +28,14 @@ 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"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	testutils "sigs.k8s.io/gateway-api-inference-extension/test/utils"
 )
 
@@ -71,182 +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).
-		SetModelName(modelName).
+		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 {
@@ -287,6 +389,10 @@ func getCurlCommand(name, ns, port, model string, timeout time.Duration, api str
 		fmt.Sprintf("%s.%s.svc:%s/v1%s", name, ns, port, api),
 		"-H",
 		"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 859a147..48cf889 100644
--- a/test/integration/epp/hermetic_test.go
+++ b/test/integration/epp/hermetic_test.go
@@ -64,6 +64,7 @@ import (
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
 	backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metrics"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
 	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/saturationdetector"
@@ -160,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},
@@ -225,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"}},
@@ -253,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.
@@ -281,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",
@@ -337,6 +322,14 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 										Key:   "hi",
 										Value: "mom",
 									},
+									{
+										Key:   metadata.ObjectiveKey,
+										Value: modelSheddable,
+									},
+									{
+										Key:   metadata.ModelNameRewriteKey,
+										Value: modelSheddableTarget,
+									},
 								},
 							},
 						},
@@ -389,6 +382,18 @@ func TestFullDuplexStreamed_KubeInferenceObjectiveRequest(t *testing.T) {
 										Key:   "hi",
 										Value: "mom",
 									},
+									{
+										Key:   metadata.ObjectiveKey,
+										Value: modelDirect,
+									},
+									{
+										Key:   metadata.ModelNameRewriteKey,
+										Value: modelDirect,
+									},
+									{
+										Key:   metadata.ModelNameRewriteKey,
+										Value: modelDirect,
+									},
 								},
 							},
 						},
@@ -748,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(
@@ -780,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(
@@ -812,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(
@@ -977,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
@@ -1086,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 996086e..0052449 100644
--- a/test/integration/util.go
+++ b/test/integration/util.go
@@ -29,12 +29,11 @@ import (
 	"github.com/go-logr/logr"
 	"google.golang.org/protobuf/types/known/structpb"
 
-	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
+	"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/metadata"
 	logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
 )
 
 const (
-	headerKeyDestination   = "x-gateway-destination-endpoint"
 	headerKeyContentLength = "Content-Length"
 )
 
@@ -112,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{
@@ -123,6 +122,14 @@ func GenerateStreamedRequestSet(logger logr.Logger, prompt, model string, filter
 							Key:   "hi",
 							Value: "mom",
 						},
+						{
+							Key:   metadata.ObjectiveKey,
+							Value: model,
+						},
+						{
+							Key:   metadata.ModelNameRewriteKey,
+							Value: targetModel,
+						},
 					},
 				},
 			},
@@ -139,32 +146,28 @@ func GenerateStreamedRequestSet(logger logr.Logger, prompt, model string, filter
 }
 
 func GenerateRequestMetadata(filterMetadata []string) map[string]*structpb.Struct {
-	metadata := make(map[string]*structpb.Struct)
+	requestMetadata := make(map[string]*structpb.Struct)
 	interfaceList := make([]any, len(filterMetadata))
 	for i, val := range filterMetadata {
 		interfaceList[i] = val
 	}
 	if filterMetadata != nil {
 		structVal, _ := structpb.NewStruct(map[string]any{
-			"x-gateway-destination-endpoint-subset": interfaceList,
+			metadata.SubsetFilterKey: interfaceList,
 		})
-		metadata["envoy.lb.subset_hint"] = structVal
+		requestMetadata[metadata.SubsetFilterNamespace] = structVal
 	}
-	return metadata
+	return requestMetadata
 }
 
 // NewRequestBufferedResponse creates a complete set of responses for the request phase.
 // It modifies request headers (e.g., for routing) and replaces the entire request body.
 // It returns a slice of two messages, representing the complete buffered action.
-func NewRequestBufferedResponse(
-	destinationEndpoint string,
-	rewrittenBody string,
-	otherHeaders ...*envoyCorev3.HeaderValueOption,
-) []*extProcPb.ProcessingResponse {
+func NewRequestBufferedResponse(destinationEndpoint string, rewrittenBody string, otherHeaders ...*envoyCorev3.HeaderValueOption) []*extProcPb.ProcessingResponse {
 	setHeaders := []*envoyCorev3.HeaderValueOption{
 		{
 			Header: &envoyCorev3.HeaderValue{
-				Key:      headerKeyDestination,
+				Key:      metadata.DestinationEndpointKey,
 				RawValue: []byte(destinationEndpoint),
 			},
 		},
@@ -214,10 +217,7 @@ func NewRequestBufferedResponse(
 // NewResponseBufferedResponse creates a complete set of responses for the response phase.
 // It modifies response headers and replaces the entire response body.
 // It is used when the processor buffers the upstream response before sending its own.
-func NewResponseBufferedResponse(
-	rewrittenBody string,
-	headersToSet ...*envoyCorev3.HeaderValueOption,
-) []*extProcPb.ProcessingResponse {
+func NewResponseBufferedResponse(rewrittenBody string, headersToSet ...*envoyCorev3.HeaderValueOption) []*extProcPb.ProcessingResponse {
 	return []*extProcPb.ProcessingResponse{
 		NewResponseHeaders(headersToSet...),
 		NewResponseStreamChunk(rewrittenBody, true),
@@ -281,11 +281,11 @@ func NewImmediateErrorResponse(code envoyTypePb.StatusCode, body string) []*extP
 func makeMetadata(endpoint string) *structpb.Struct {
 	return &structpb.Struct{
 		Fields: map[string]*structpb.Value{
-			server.DefaultDestinationEndpointHintMetadataNamespace: {
+			metadata.DestinationEndpointNamespace: {
 				Kind: &structpb.Value_StructValue{
 					StructValue: &structpb.Struct{
 						Fields: map[string]*structpb.Value{
-							server.DefaultDestinationEndpointHintKey: {
+							metadata.DestinationEndpointKey: {
 								Kind: &structpb.Value_StringValue{
 									StringValue: endpoint,
 								},
diff --git a/test/testdata/inferencepool-e2e.yaml b/test/testdata/inferencepool-e2e.yaml
index a6852a3..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
@@ -109,8 +111,6 @@ data:
     - type: queue-scorer
     - type: kv-cache-utilization-scorer
     - type: prefix-cache-scorer
-    - type: max-score-picker
-    - type: single-profile-handler
     schedulingProfiles:
     - name: default
       plugins:
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 21124ed..20fea8d 100644
--- a/test/testdata/inferencepool-with-model-hermetic.yaml
+++ b/test/testdata/inferencepool-with-model-hermetic.yaml
@@ -4,20 +4,21 @@ 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
 ---
 apiVersion: inference.networking.x-k8s.io/v1alpha2
 kind: InferenceObjective
 metadata:
-  name: sample
+  name: sql-lora
   namespace: default
 spec:
-  modelName: sql-lora
-  criticality: Critical
+  priority: 2
   poolRef:
     name: vllm-llama3-8b-instruct-pool
   targetModels:
@@ -27,10 +28,9 @@ spec:
 apiVersion: inference.networking.x-k8s.io/v1alpha2
 kind: InferenceObjective
 metadata:
-  name: sheddable
+  name: sql-lora-sheddable
   namespace: default
 spec:
-  modelName: sql-lora-sheddable
   poolRef:
     name: vllm-llama3-8b-instruct-pool
   targetModels:
@@ -40,11 +40,10 @@ spec:
 apiVersion: inference.networking.x-k8s.io/v1alpha2
 kind: InferenceObjective
 metadata:
-  name: generic
+  name: my-model
   namespace: default
 spec:
-  modelName: my-model
-  criticality: Critical
+  priority: 2
   poolRef:
     name: vllm-llama3-8b-instruct-pool
   targetModels:
@@ -54,10 +53,9 @@ spec:
 apiVersion: inference.networking.x-k8s.io/v1alpha2
 kind: InferenceObjective
 metadata:
-  name: direct-model-name
+  name: direct-model
   namespace: default
 spec:
-  modelName: direct-model
-  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 54f6a04..51eb33f 100644
--- a/test/utils/server.go
+++ b/test/utils/server.go
@@ -49,13 +49,13 @@ 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{}
 	for _, objective := range objectives {
 		initObjs = append(initObjs, objective)
-		ds.ObjectiveSetIfOlder(objective)
+		ds.ObjectiveSet(objective)
 	}
 	for _, pod := range pods {
 		initObjs = append(initObjs, pod)
@@ -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 d79d98e..f1c0098 100644
--- a/test/utils/wrappers.go
+++ b/test/utils/wrappers.go
@@ -38,22 +38,15 @@ func MakeModelWrapper(namespacedName types.NamespacedName) *InferenceObjectiveWr
 				Namespace: namespacedName.Namespace,
 			},
 			Spec: v1alpha2.InferenceObjectiveSpec{
-				ModelName: "",
-				PoolRef:   v1alpha2.PoolObjectReference{},
+				PoolRef: v1alpha2.PoolObjectReference{},
 			},
 		},
 	}
 }
 
-// SetModelName sets the value of the InferenceObjective.spec.modelName.
-func (m *InferenceObjectiveWrapper) SetModelName(name string) *InferenceObjectiveWrapper {
-	m.Spec.ModelName = name
-	return m
-}
-
-// 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
 }
 
@@ -69,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