Repository: envoyproxy/ai-gateway
Pull request #364 (2025-02-18 22:19:03)

controller: removes sink implementation
**Commit Message**

This removes the config sink style implementation of controller. Previously, there the config sink was used as a central place to do the mutation that involves multiple resources. However, it unnecessarily comes with the complexity and this simplifies the implementation by removing it. 

Majority of the changes are code moving, especially moving the resource mutation code from sink to under AIGatewayRoute controller.

This was brought up in the conversation with @sanposhiho and more follow up PRs will come to polish the implementation.



Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 4b4ac85..fa47d11 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -60,6 +60,10 @@ linters-settings:
         alias: mcsapiv1a1
       - pkg: k8s.io/api/(\w+)/(v[\w\d]+)
         alias: $1$2
+      - pkg: k8s.io/apimachinery/pkg/api/errors
+        alias: apierrors
+      - pkg: github.com/envoyproxy/ai-gateway/internal/testing
+        alias: internaltesting
   gci:
     sections:
       # Captures all standard packages if they do not match another section.
diff --git a/Makefile b/Makefile
index e030a83..7fe03c0 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ HELM_CHART_VERSION ?= v0.0.0-latest
 
 # Arguments for go test. This can be used, for example, to run specific tests via
 # `GO_TEST_ARGS="-run TestName/foo/etc -v -race"`.
-GO_TEST_ARGS ?= -v -race
+GO_TEST_ARGS ?= -race
 # Arguments for go test in e2e tests in addition to GO_TEST_ARGS, applicable to test-e2e, test-extproc, and test-controller.
 GO_TEST_E2E_ARGS ?= -count=1
 
@@ -151,7 +151,7 @@ test-extproc: build.extproc
 	@$(MAKE) build.extproc_custom_router CMD_PATH_PREFIX=examples
 	@$(MAKE) build.testupstream CMD_PATH_PREFIX=tests/internal/testupstreamlib
 	@echo "Run ExtProc test"
-	@go test ./tests/extproc/... $(GO_TEST_ARGS) $(GO_TEST_E2E_ARGS) -tags test_extproc -v
+	@go test ./tests/extproc/... $(GO_TEST_ARGS) $(GO_TEST_E2E_ARGS) -tags test_extproc
 
 # This runs the end-to-end tests for the controller with EnvTest.
 .PHONY: test-controller
diff --git a/go.mod b/go.mod
index 84bde1e..d16c36e 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,6 @@ require (
 	k8s.io/apiextensions-apiserver v0.32.2
 	k8s.io/apimachinery v0.32.2
 	k8s.io/client-go v0.32.2
-	k8s.io/klog/v2 v2.130.1
 	k8s.io/utils v0.0.0-20241210054802-24370beab758
 	sigs.k8s.io/controller-runtime v0.20.2
 	sigs.k8s.io/gateway-api v1.2.1
@@ -374,6 +373,7 @@ require (
 	k8s.io/apiserver v0.32.2 // indirect
 	k8s.io/cli-runtime v0.32.1 // indirect
 	k8s.io/component-base v0.32.2 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
 	k8s.io/kubectl v0.32.1 // indirect
 	mvdan.cc/gofumpt v0.7.0 // indirect
diff --git a/internal/controller/ai_gateway_route.go b/internal/controller/ai_gateway_route.go
index b6b22c0..9e1a03c 100644
--- a/internal/controller/ai_gateway_route.go
+++ b/internal/controller/ai_gateway_route.go
@@ -8,6 +8,7 @@ package controller
 import (
 	"context"
 	"fmt"
+	"path"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/go-logr/logr"
@@ -15,52 +16,68 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	uuid2 "k8s.io/apimachinery/pkg/util/uuid"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/yaml"
 
 	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
 	"github.com/envoyproxy/ai-gateway/filterapi"
+	"github.com/envoyproxy/ai-gateway/internal/controller/rotators"
+	"github.com/envoyproxy/ai-gateway/internal/llmcostcel"
 )
 
 const (
-	managedByLabel        = "app.kubernetes.io/managed-by"
-	expProcConfigFileName = "extproc-config.yaml"
+	managedByLabel             = "app.kubernetes.io/managed-by"
+	expProcConfigFileName      = "extproc-config.yaml"
+	selectedBackendHeaderKey   = "x-ai-eg-selected-backend"
+	hostRewriteHTTPFilterName  = "ai-eg-host-rewrite"
+	extProcConfigAnnotationKey = "aigateway.envoyproxy.io/extproc-config-uuid"
+	// mountedExtProcSecretPath specifies the secret file mounted on the external proc. The idea is to update the mounted.
+	//
+	//	secret with backendSecurityPolicy auth instead of mounting new secret files to the external proc.
+	mountedExtProcSecretPath = "/etc/backend_security_policy" // #nosec G101
 )
 
-// aiGatewayRouteController implements [reconcile.TypedReconciler].
+// AIGatewayRouteController implements [reconcile.TypedReconciler].
 //
 // This handles the AIGatewayRoute resource and creates the necessary resources for the external process.
-type aiGatewayRouteController struct {
-	client    client.Client
-	kube      kubernetes.Interface
-	logger    logr.Logger
-	eventChan chan ConfigSinkEvent
+//
+// Exported for testing purposes.
+type AIGatewayRouteController struct {
+	client client.Client
+	kube   kubernetes.Interface
+	logger logr.Logger
+
+	extProcImage           string
+	extProcImagePullPolicy corev1.PullPolicy
+	extProcLogLevel        string
 }
 
 // NewAIGatewayRouteController creates a new reconcile.TypedReconciler[reconcile.Request] for the AIGatewayRoute resource.
 func NewAIGatewayRouteController(
 	client client.Client, kube kubernetes.Interface, logger logr.Logger,
-	ch chan ConfigSinkEvent,
-) reconcile.TypedReconciler[reconcile.Request] {
-	return &aiGatewayRouteController{
-		client:    client,
-		kube:      kube,
-		logger:    logger,
-		eventChan: ch,
+	extProcImage, extProcLogLevel string,
+) *AIGatewayRouteController {
+	return &AIGatewayRouteController{
+		client:                 client,
+		kube:                   kube,
+		logger:                 logger,
+		extProcImage:           extProcImage,
+		extProcImagePullPolicy: corev1.PullIfNotPresent,
+		extProcLogLevel:        extProcLogLevel,
 	}
 }
 
 // Reconcile implements [reconcile.TypedReconciler].
-//
-// This only creates the external process deployment and service for the AIGatewayRoute as well as the extension policy.
-// The actual HTTPRoute and the extproc configuration will be created in the config sink since we need
-// not only the AIGatewayRoute but also the AIServiceBackend and other resources to create the full configuration.
-func (c *aiGatewayRouteController) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
+func (c *AIGatewayRouteController) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
 	logger := log.FromContext(ctx)
 	logger.Info("Reconciling AIGatewayRoute", "namespace", req.Namespace, "name", req.Name)
 
@@ -74,21 +91,22 @@ func (c *aiGatewayRouteController) Reconcile(ctx context.Context, req reconcile.
 		return ctrl.Result{}, err
 	}
 
+	// TODO: merge this into syncAIGatewayRoute. This is a left over from the previous sink based implementation.
+	c.logger.Info("Ensuring extproc configmap exists", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
 	if err := c.ensuresExtProcConfigMapExists(ctx, &aiGatewayRoute); err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to ensure extproc configmap exists: %w", err)
 	}
-
+	// TODO: merge this into syncAIGatewayRoute. This is a left over from the previous sink based implementation.
+	c.logger.Info("Reconciling extension policy", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
 	if err := c.reconcileExtProcExtensionPolicy(ctx, &aiGatewayRoute); err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to reconcile extension policy: %w", err)
 	}
-	// Send a copy to the config sink for a full reconciliation on HTTPRoute as well as the extproc config.
-	c.eventChan <- aiGatewayRoute.DeepCopy()
-	return reconcile.Result{}, nil
+	return reconcile.Result{}, c.syncAIGatewayRoute(ctx, &aiGatewayRoute)
 }
 
 // reconcileExtProcExtensionPolicy creates or updates the extension policy for the external process.
 // It only changes the target references.
-func (c *aiGatewayRouteController) reconcileExtProcExtensionPolicy(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) (err error) {
+func (c *AIGatewayRouteController) reconcileExtProcExtensionPolicy(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) (err error) {
 	var existingPolicy egv1a1.EnvoyExtensionPolicy
 	if err = c.client.Get(ctx, client.ObjectKey{Name: extProcName(aiGatewayRoute), Namespace: aiGatewayRoute.Namespace}, &existingPolicy); err == nil {
 		existingPolicy.Spec.PolicyTargetReferences.TargetRefs = aiGatewayRoute.Spec.TargetRefs
@@ -137,7 +155,7 @@ func (c *aiGatewayRouteController) reconcileExtProcExtensionPolicy(ctx context.C
 
 // ensuresExtProcConfigMapExists ensures that a configmap exists for the external process.
 // This must happen before the external process deployment is created.
-func (c *aiGatewayRouteController) ensuresExtProcConfigMapExists(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) (err error) {
+func (c *AIGatewayRouteController) ensuresExtProcConfigMapExists(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) (err error) {
 	name := extProcName(aiGatewayRoute)
 	// Check if a configmap exists for extproc exists, and if not, create one with the default config.
 	_, err = c.kube.CoreV1().ConfigMaps(aiGatewayRoute.Namespace).Get(ctx, name, metav1.GetOptions{})
@@ -173,3 +191,487 @@ func applyExtProcDeploymentConfigUpdate(d *appsv1.DeploymentSpec, filterConfig *
 		d.Replicas = replica
 	}
 }
+
+// syncAIGatewayRoute implements syncAIGatewayRouteFn.
+func (c *AIGatewayRouteController) syncAIGatewayRoute(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) error {
+	// Check if the HTTPRouteFilter exists in the namespace.
+	var httpRouteFilter egv1a1.HTTPRouteFilter
+	err := c.client.Get(ctx,
+		client.ObjectKey{Name: hostRewriteHTTPFilterName, Namespace: aiGatewayRoute.Namespace}, &httpRouteFilter)
+	if apierrors.IsNotFound(err) {
+		httpRouteFilter = egv1a1.HTTPRouteFilter{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      hostRewriteHTTPFilterName,
+				Namespace: aiGatewayRoute.Namespace,
+			},
+			Spec: egv1a1.HTTPRouteFilterSpec{
+				URLRewrite: &egv1a1.HTTPURLRewriteFilter{
+					Hostname: &egv1a1.HTTPHostnameModifier{
+						Type: egv1a1.BackendHTTPHostnameModifier,
+					},
+				},
+			},
+		}
+		if err = c.client.Create(ctx, &httpRouteFilter); err != nil {
+			return fmt.Errorf("failed to create HTTPRouteFilter: %w", err)
+		}
+	} else if err != nil {
+		return fmt.Errorf("failed to get HTTPRouteFilter: %w", err)
+	}
+
+	// Check if the HTTPRoute exists.
+	c.logger.Info("syncing AIGatewayRoute", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
+	var httpRoute gwapiv1.HTTPRoute
+	err = c.client.Get(ctx, client.ObjectKey{Name: aiGatewayRoute.Name, Namespace: aiGatewayRoute.Namespace}, &httpRoute)
+	existingRoute := err == nil
+	if apierrors.IsNotFound(err) {
+		// This means that this AIGatewayRoute is a new one.
+		httpRoute = gwapiv1.HTTPRoute{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      aiGatewayRoute.Name,
+				Namespace: aiGatewayRoute.Namespace,
+			},
+			Spec: gwapiv1.HTTPRouteSpec{},
+		}
+		if err = ctrlutil.SetControllerReference(aiGatewayRoute, &httpRoute, c.client.Scheme()); err != nil {
+			panic(fmt.Errorf("BUG: failed to set controller reference for HTTPRoute: %w", err))
+		}
+	} else if err != nil {
+		return fmt.Errorf("failed to get HTTPRoute: %w", err)
+	}
+
+	// Update the HTTPRoute with the new AIGatewayRoute.
+	if err = c.newHTTPRoute(ctx, &httpRoute, aiGatewayRoute); err != nil {
+		return fmt.Errorf("failed to construct a new HTTPRoute: %w", err)
+	}
+
+	if existingRoute {
+		c.logger.Info("updating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
+		if err = c.client.Update(ctx, &httpRoute); err != nil {
+			return fmt.Errorf("failed to update HTTPRoute: %w", err)
+		}
+	} else {
+		c.logger.Info("creating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
+		if err = c.client.Create(ctx, &httpRoute); err != nil {
+			return fmt.Errorf("failed to create HTTPRoute: %w", err)
+		}
+	}
+
+	// Update the extproc configmap.
+	uuid := string(uuid2.NewUUID())
+	if err = c.updateExtProcConfigMap(ctx, aiGatewayRoute, uuid); err != nil {
+		return fmt.Errorf("failed to update extproc configmap: %w", err)
+	}
+
+	// Deploy extproc deployment with potential updates.
+	err = c.syncExtProcDeployment(ctx, aiGatewayRoute)
+	if err != nil {
+		return fmt.Errorf("failed to sync extproc deployment: %w", err)
+	}
+
+	// Annotate all pods with the new config.
+	err = c.annotateExtProcPods(ctx, aiGatewayRoute, uuid)
+	if err != nil {
+		return fmt.Errorf("failed to annotate extproc pods: %w", err)
+	}
+	return nil
+}
+
+// updateExtProcConfigMap updates the external process configmap with the new AIGatewayRoute.
+func (c *AIGatewayRouteController) updateExtProcConfigMap(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute, uuid string) error {
+	configMap, err := c.kube.CoreV1().ConfigMaps(aiGatewayRoute.Namespace).Get(ctx, extProcName(aiGatewayRoute), metav1.GetOptions{})
+	if err != nil {
+		// This is a bug since we should have created the configmap before sending the AIGatewayRoute to the configSink.
+		panic(fmt.Errorf("failed to get configmap %s: %w", extProcName(aiGatewayRoute), err))
+	}
+
+	ec := &filterapi.Config{UUID: uuid}
+	spec := &aiGatewayRoute.Spec
+
+	ec.Schema.Name = filterapi.APISchemaName(spec.APISchema.Name)
+	ec.Schema.Version = spec.APISchema.Version
+	ec.ModelNameHeaderKey = aigv1a1.AIModelHeaderKey
+	ec.SelectedBackendHeaderKey = selectedBackendHeaderKey
+	ec.Rules = make([]filterapi.RouteRule, len(spec.Rules))
+	for i := range spec.Rules {
+		rule := &spec.Rules[i]
+		ec.Rules[i].Backends = make([]filterapi.Backend, len(rule.BackendRefs))
+		for j := range rule.BackendRefs {
+			backend := &rule.BackendRefs[j]
+			key := fmt.Sprintf("%s.%s", backend.Name, aiGatewayRoute.Namespace)
+			ec.Rules[i].Backends[j].Name = key
+			ec.Rules[i].Backends[j].Weight = backend.Weight
+			var backendObj *aigv1a1.AIServiceBackend
+			backendObj, err = c.backend(ctx, aiGatewayRoute.Namespace, backend.Name)
+			if err != nil {
+				return fmt.Errorf("failed to get AIServiceBackend %s: %w", key, err)
+			}
+			ec.Rules[i].Backends[j].Schema.Name = filterapi.APISchemaName(backendObj.Spec.APISchema.Name)
+			ec.Rules[i].Backends[j].Schema.Version = backendObj.Spec.APISchema.Version
+
+			if bspRef := backendObj.Spec.BackendSecurityPolicyRef; bspRef != nil {
+				volumeName := backendSecurityPolicyVolumeName(
+					i, j, string(backendObj.Spec.BackendSecurityPolicyRef.Name),
+				)
+				var backendSecurityPolicy *aigv1a1.BackendSecurityPolicy
+				backendSecurityPolicy, err = c.backendSecurityPolicy(ctx, aiGatewayRoute.Namespace, string(bspRef.Name))
+				if err != nil {
+					return fmt.Errorf("failed to get BackendSecurityPolicy %s: %w", bspRef.Name, err)
+				}
+
+				switch backendSecurityPolicy.Spec.Type {
+				case aigv1a1.BackendSecurityPolicyTypeAPIKey:
+					ec.Rules[i].Backends[j].Auth = &filterapi.BackendAuth{
+						APIKey: &filterapi.APIKeyAuth{Filename: path.Join(backendSecurityMountPath(volumeName), "/apiKey")},
+					}
+				case aigv1a1.BackendSecurityPolicyTypeAWSCredentials:
+					if backendSecurityPolicy.Spec.AWSCredentials == nil {
+						return fmt.Errorf("AWSCredentials type selected but not defined %s", backendSecurityPolicy.Name)
+					}
+					if awsCred := backendSecurityPolicy.Spec.AWSCredentials; awsCred.CredentialsFile != nil || awsCred.OIDCExchangeToken != nil {
+						ec.Rules[i].Backends[j].Auth = &filterapi.BackendAuth{
+							AWSAuth: &filterapi.AWSAuth{
+								CredentialFileName: path.Join(backendSecurityMountPath(volumeName), "/credentials"),
+								Region:             backendSecurityPolicy.Spec.AWSCredentials.Region,
+							},
+						}
+					}
+				default:
+					return fmt.Errorf("invalid backend security type %s for policy %s", backendSecurityPolicy.Spec.Type,
+						backendSecurityPolicy.Name)
+				}
+			}
+		}
+		ec.Rules[i].Headers = make([]filterapi.HeaderMatch, len(rule.Matches))
+		for j, match := range rule.Matches {
+			ec.Rules[i].Headers[j].Name = match.Headers[0].Name
+			ec.Rules[i].Headers[j].Value = match.Headers[0].Value
+		}
+	}
+
+	ec.MetadataNamespace = aigv1a1.AIGatewayFilterMetadataNamespace
+	for _, cost := range aiGatewayRoute.Spec.LLMRequestCosts {
+		fc := filterapi.LLMRequestCost{MetadataKey: cost.MetadataKey}
+		switch cost.Type {
+		case aigv1a1.LLMRequestCostTypeInputToken:
+			fc.Type = filterapi.LLMRequestCostTypeInputToken
+		case aigv1a1.LLMRequestCostTypeOutputToken:
+			fc.Type = filterapi.LLMRequestCostTypeOutputToken
+		case aigv1a1.LLMRequestCostTypeTotalToken:
+			fc.Type = filterapi.LLMRequestCostTypeTotalToken
+		case aigv1a1.LLMRequestCostTypeCEL:
+			fc.Type = filterapi.LLMRequestCostTypeCELExpression
+			expr := *cost.CELExpression
+			// Sanity check the CEL expression.
+			_, err = llmcostcel.NewProgram(expr)
+			if err != nil {
+				return fmt.Errorf("invalid CEL expression: %w", err)
+			}
+			fc.CELExpression = expr
+		default:
+			return fmt.Errorf("unknown request cost type: %s", cost.Type)
+		}
+		ec.LLMRequestCosts = append(ec.LLMRequestCosts, fc)
+	}
+
+	marshaled, err := yaml.Marshal(ec)
+	if err != nil {
+		return fmt.Errorf("failed to marshal extproc config: %w", err)
+	}
+	if configMap.Data == nil {
+		configMap.Data = make(map[string]string)
+	}
+	configMap.Data[expProcConfigFileName] = string(marshaled)
+	if _, err := c.kube.CoreV1().ConfigMaps(aiGatewayRoute.Namespace).Update(ctx, configMap, metav1.UpdateOptions{}); err != nil {
+		return fmt.Errorf("failed to update configmap %s: %w", configMap.Name, err)
+	}
+	return nil
+}
+
+// newHTTPRoute updates the HTTPRoute with the new AIGatewayRoute.
+func (c *AIGatewayRouteController) newHTTPRoute(ctx context.Context, dst *gwapiv1.HTTPRoute, aiGatewayRoute *aigv1a1.AIGatewayRoute) error {
+	var backends []*aigv1a1.AIServiceBackend
+	dedup := make(map[string]struct{})
+	for _, rule := range aiGatewayRoute.Spec.Rules {
+		for _, br := range rule.BackendRefs {
+			key := fmt.Sprintf("%s.%s", br.Name, aiGatewayRoute.Namespace)
+			if _, ok := dedup[key]; ok {
+				continue
+			}
+			dedup[key] = struct{}{}
+			backend, err := c.backend(ctx, aiGatewayRoute.Namespace, br.Name)
+			if err != nil {
+				return fmt.Errorf("AIServiceBackend %s not found", key)
+			}
+			backends = append(backends, backend)
+		}
+	}
+
+	rewriteFilters := []gwapiv1.HTTPRouteFilter{
+		{
+			Type: gwapiv1.HTTPRouteFilterExtensionRef,
+			ExtensionRef: &gwapiv1.LocalObjectReference{
+				Group: "gateway.envoyproxy.io",
+				Kind:  "HTTPRouteFilter",
+				Name:  hostRewriteHTTPFilterName,
+			},
+		},
+	}
+	rules := make([]gwapiv1.HTTPRouteRule, len(backends))
+	for i, b := range backends {
+		key := fmt.Sprintf("%s.%s", b.Name, b.Namespace)
+		rule := gwapiv1.HTTPRouteRule{
+			BackendRefs: []gwapiv1.HTTPBackendRef{
+				{BackendRef: gwapiv1.BackendRef{BackendObjectReference: b.Spec.BackendRef}},
+			},
+			Matches: []gwapiv1.HTTPRouteMatch{
+				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: key}}},
+			},
+			Filters: rewriteFilters,
+		}
+		rules[i] = rule
+	}
+
+	// Adds the default route rule with "/" path.
+	if len(rules) > 0 {
+		rules = append(rules, gwapiv1.HTTPRouteRule{
+			Matches: []gwapiv1.HTTPRouteMatch{
+				{Path: &gwapiv1.HTTPPathMatch{Value: ptr.To("/")}},
+			},
+			BackendRefs: []gwapiv1.HTTPBackendRef{
+				{BackendRef: gwapiv1.BackendRef{BackendObjectReference: backends[0].Spec.BackendRef}},
+			},
+			Filters: rewriteFilters,
+		})
+	}
+
+	dst.Spec.Rules = rules
+
+	targetRefs := aiGatewayRoute.Spec.TargetRefs
+	egNs := gwapiv1.Namespace(aiGatewayRoute.Namespace)
+	parentRefs := make([]gwapiv1.ParentReference, len(targetRefs))
+	for i, egRef := range targetRefs {
+		egName := egRef.Name
+		parentRefs[i] = gwapiv1.ParentReference{
+			Name:      egName,
+			Namespace: &egNs,
+		}
+	}
+	dst.Spec.CommonRouteSpec.ParentRefs = parentRefs
+	return nil
+}
+
+// annotateExtProcPods annotates the external processor pods with the new config uuid.
+// This is necessary to make the config update faster.
+//
+// See https://neonmirrors.net/post/2022-12/reducing-pod-volume-update-times/ for explanation.
+func (c *AIGatewayRouteController) annotateExtProcPods(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute, uuid string) error {
+	pods, err := c.kube.CoreV1().Pods(aiGatewayRoute.Namespace).List(ctx, metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("app=%s", extProcName(aiGatewayRoute)),
+	})
+	if err != nil {
+		return fmt.Errorf("failed to list pods: %w", err)
+	}
+
+	for _, pod := range pods.Items {
+		c.logger.Info("annotating pod", "namespace", pod.Namespace, "name", pod.Name)
+		_, err = c.kube.CoreV1().Pods(pod.Namespace).Patch(ctx, pod.Name, types.MergePatchType,
+			[]byte(fmt.Sprintf(
+				`{"metadata":{"annotations":{"%s":"%s"}}}`, extProcConfigAnnotationKey, uuid),
+			), metav1.PatchOptions{})
+		if err != nil {
+			return fmt.Errorf("failed to patch pod %s: %w", pod.Name, err)
+		}
+	}
+	return nil
+}
+
+// syncExtProcDeployment syncs the external processor's Deployment and Service.
+func (c *AIGatewayRouteController) syncExtProcDeployment(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) error {
+	name := extProcName(aiGatewayRoute)
+	labels := map[string]string{"app": name, managedByLabel: "envoy-ai-gateway"}
+
+	deployment, err := c.kube.AppsV1().Deployments(aiGatewayRoute.Namespace).Get(ctx, extProcName(aiGatewayRoute), metav1.GetOptions{})
+	if err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			deployment = &appsv1.Deployment{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      name,
+					Namespace: aiGatewayRoute.Namespace,
+					Labels:    labels,
+				},
+				Spec: appsv1.DeploymentSpec{
+					Selector: &metav1.LabelSelector{MatchLabels: labels},
+					Template: corev1.PodTemplateSpec{
+						ObjectMeta: metav1.ObjectMeta{Labels: labels},
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:            name,
+									Image:           c.extProcImage,
+									ImagePullPolicy: c.extProcImagePullPolicy,
+									Ports:           []corev1.ContainerPort{{Name: "grpc", ContainerPort: 1063}},
+									Args: []string{
+										"-configPath", "/etc/ai-gateway/extproc/" + expProcConfigFileName,
+										"-logLevel", c.extProcLogLevel,
+									},
+									VolumeMounts: []corev1.VolumeMount{
+										{
+											Name:      "config",
+											MountPath: "/etc/ai-gateway/extproc",
+											ReadOnly:  true,
+										},
+									},
+								},
+							},
+							Volumes: []corev1.Volume{
+								{
+									Name: "config",
+									VolumeSource: corev1.VolumeSource{
+										ConfigMap: &corev1.ConfigMapVolumeSource{
+											LocalObjectReference: corev1.LocalObjectReference{Name: extProcName(aiGatewayRoute)},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+			if err = ctrlutil.SetControllerReference(aiGatewayRoute, deployment, c.client.Scheme()); err != nil {
+				panic(fmt.Errorf("BUG: failed to set controller reference for deployment: %w", err))
+			}
+			var updatedSpec *corev1.PodSpec
+			updatedSpec, err = c.mountBackendSecurityPolicySecrets(ctx, &deployment.Spec.Template.Spec, aiGatewayRoute)
+			if err == nil {
+				deployment.Spec.Template.Spec = *updatedSpec
+			}
+			applyExtProcDeploymentConfigUpdate(&deployment.Spec, aiGatewayRoute.Spec.FilterConfig)
+			_, err = c.kube.AppsV1().Deployments(aiGatewayRoute.Namespace).Create(ctx, deployment, metav1.CreateOptions{})
+			if err != nil {
+				return fmt.Errorf("failed to create deployment: %w", err)
+			}
+			c.logger.Info("Created deployment", "name", name)
+		} else {
+			return fmt.Errorf("failed to get deployment: %w", err)
+		}
+	} else {
+		var updatedSpec *corev1.PodSpec
+		updatedSpec, err = c.mountBackendSecurityPolicySecrets(ctx, &deployment.Spec.Template.Spec, aiGatewayRoute)
+		if err == nil {
+			deployment.Spec.Template.Spec = *updatedSpec
+		}
+		applyExtProcDeploymentConfigUpdate(&deployment.Spec, aiGatewayRoute.Spec.FilterConfig)
+		if _, err = c.kube.AppsV1().Deployments(aiGatewayRoute.Namespace).Update(ctx, deployment, metav1.UpdateOptions{}); err != nil {
+			return fmt.Errorf("failed to update deployment: %w", err)
+		}
+	}
+
+	// This is static, so we don't need to update it.
+	service := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: aiGatewayRoute.Namespace,
+			Labels:    labels,
+		},
+		Spec: corev1.ServiceSpec{
+			Selector: labels,
+			Ports: []corev1.ServicePort{
+				{
+					Name:        "grpc",
+					Protocol:    corev1.ProtocolTCP,
+					Port:        1063,
+					AppProtocol: ptr.To("grpc"),
+				},
+			},
+		},
+	}
+	if err = ctrlutil.SetControllerReference(aiGatewayRoute, service, c.client.Scheme()); err != nil {
+		panic(fmt.Errorf("BUG: failed to set controller reference for service: %w", err))
+	}
+	if _, err = c.kube.CoreV1().Services(aiGatewayRoute.Namespace).Create(ctx, service, metav1.CreateOptions{}); client.IgnoreAlreadyExists(err) != nil {
+		return fmt.Errorf("failed to create Service %s.%s: %w", name, aiGatewayRoute.Namespace, err)
+	}
+	return nil
+}
+
+// mountBackendSecurityPolicySecrets will mount secrets based on backendSecurityPolicies attached to AIServiceBackend.
+func (c *AIGatewayRouteController) mountBackendSecurityPolicySecrets(ctx context.Context, spec *corev1.PodSpec, aiGatewayRoute *aigv1a1.AIGatewayRoute) (*corev1.PodSpec, error) {
+	// Mount from scratch to avoid secrets that should be unmounted.
+	// Only keep the original mount which should be the config volume.
+	spec.Volumes = spec.Volumes[:1]
+	container := &spec.Containers[0]
+	container.VolumeMounts = container.VolumeMounts[:1]
+
+	for i := range aiGatewayRoute.Spec.Rules {
+		rule := &aiGatewayRoute.Spec.Rules[i]
+		for j := range rule.BackendRefs {
+			backendRef := &rule.BackendRefs[j]
+			backend, err := c.backend(ctx, aiGatewayRoute.Namespace, backendRef.Name)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get backend %s: %w", backendRef.Name, err)
+			}
+
+			if backendSecurityPolicyRef := backend.Spec.BackendSecurityPolicyRef; backendSecurityPolicyRef != nil {
+				backendSecurityPolicy, err := c.backendSecurityPolicy(ctx, aiGatewayRoute.Namespace, string(backendSecurityPolicyRef.Name))
+				if err != nil {
+					return nil, fmt.Errorf("failed to get backend security policy %s: %w", backendSecurityPolicyRef.Name, err)
+				}
+
+				var secretName string
+				switch backendSecurityPolicy.Spec.Type {
+				case aigv1a1.BackendSecurityPolicyTypeAPIKey:
+					secretName = string(backendSecurityPolicy.Spec.APIKey.SecretRef.Name)
+				case aigv1a1.BackendSecurityPolicyTypeAWSCredentials:
+					if awsCred := backendSecurityPolicy.Spec.AWSCredentials; awsCred.CredentialsFile != nil {
+						secretName = string(backendSecurityPolicy.Spec.AWSCredentials.CredentialsFile.SecretRef.Name)
+					} else {
+						secretName = rotators.GetBSPSecretName(backendSecurityPolicy.Name)
+					}
+				default:
+					return nil, fmt.Errorf("backend security policy %s is not supported", backendSecurityPolicy.Spec.Type)
+				}
+
+				volumeName := backendSecurityPolicyVolumeName(i, j, string(backend.Spec.BackendSecurityPolicyRef.Name))
+				spec.Volumes = append(spec.Volumes, corev1.Volume{
+					Name: volumeName,
+					VolumeSource: corev1.VolumeSource{
+						Secret: &corev1.SecretVolumeSource{SecretName: secretName},
+					},
+				})
+
+				container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
+					Name:      volumeName,
+					MountPath: backendSecurityMountPath(volumeName),
+					ReadOnly:  true,
+				})
+			}
+		}
+	}
+	return spec, nil
+}
+
+func (c *AIGatewayRouteController) backend(ctx context.Context, namespace, name string) (*aigv1a1.AIServiceBackend, error) {
+	backend := &aigv1a1.AIServiceBackend{}
+	if err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, backend); err != nil {
+		return nil, err
+	}
+	return backend, nil
+}
+
+func (c *AIGatewayRouteController) backendSecurityPolicy(ctx context.Context, namespace, name string) (*aigv1a1.BackendSecurityPolicy, error) {
+	backendSecurityPolicy := &aigv1a1.BackendSecurityPolicy{}
+	if err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, backendSecurityPolicy); err != nil {
+		return nil, err
+	}
+	return backendSecurityPolicy, nil
+}
+
+func backendSecurityPolicyVolumeName(ruleIndex, backendRefIndex int, name string) string {
+	// Note: do not use "." as it's not allowed in the volume name.
+	return fmt.Sprintf("rule%d-backref%d-%s", ruleIndex, backendRefIndex, name)
+}
+
+func backendSecurityMountPath(backendSecurityPolicyKey string) string {
+	return fmt.Sprintf("%s/%s", mountedExtProcSecretPath, backendSecurityPolicyKey)
+}
diff --git a/internal/controller/ai_service_backend.go b/internal/controller/ai_service_backend.go
index 0c025c2..1574718 100644
--- a/internal/controller/ai_service_backend.go
+++ b/internal/controller/ai_service_backend.go
@@ -7,6 +7,8 @@ package controller
 
 import (
 	"context"
+	"errors"
+	"fmt"
 
 	"github.com/go-logr/logr"
 	"k8s.io/client-go/kubernetes"
@@ -17,38 +19,61 @@ import (
 	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
 )
 
-// aiBackendController implements [reconcile.TypedReconciler] for [aigv1a1.AIServiceBackend].
+// AIBackendController implements [reconcile.TypedReconciler] for [aigv1a1.AIServiceBackend].
 //
-// This handles the AIServiceBackend resource and sends it to the config sink so that it can modify the configuration together with the state of other resources.
-type aiBackendController struct {
+// Exported for testing purposes.
+type AIBackendController struct {
 	client    client.Client
 	kube      kubernetes.Interface
 	logger    logr.Logger
-	eventChan chan ConfigSinkEvent
+	syncRoute syncAIGatewayRouteFn
 }
 
 // NewAIServiceBackendController creates a new [reconcile.TypedReconciler] for [aigv1a1.AIServiceBackend].
-func NewAIServiceBackendController(client client.Client, kube kubernetes.Interface, logger logr.Logger, ch chan ConfigSinkEvent) reconcile.TypedReconciler[reconcile.Request] {
-	return &aiBackendController{
+func NewAIServiceBackendController(client client.Client, kube kubernetes.Interface, logger logr.Logger, syncRoute syncAIGatewayRouteFn) *AIBackendController {
+	return &AIBackendController{
 		client:    client,
 		kube:      kube,
 		logger:    logger,
-		eventChan: ch,
+		syncRoute: syncRoute,
 	}
 }
 
 // Reconcile implements the [reconcile.TypedReconciler] for [aigv1a1.AIServiceBackend].
-func (l *aiBackendController) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
+func (c *AIBackendController) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
 	var aiBackend aigv1a1.AIServiceBackend
-	if err := l.client.Get(ctx, req.NamespacedName, &aiBackend); err != nil {
+	if err := c.client.Get(ctx, req.NamespacedName, &aiBackend); err != nil {
 		if client.IgnoreNotFound(err) == nil {
-			l.logger.Info("Deleting AIServiceBackend",
+			c.logger.Info("Deleting AIServiceBackend",
 				"namespace", req.Namespace, "name", req.Name)
 			return ctrl.Result{}, nil
 		}
 		return ctrl.Result{}, err
 	}
-	// Send the AIServiceBackend to the config sink so that it can modify the configuration together with the state of other resources.
-	l.eventChan <- aiBackend.DeepCopy()
-	return ctrl.Result{}, nil
+	c.logger.Info("Reconciling AIServiceBackend", "namespace", req.Namespace, "name", req.Name)
+	return ctrl.Result{}, c.syncAIServiceBackend(ctx, &aiBackend)
+}
+
+// syncAIServiceBackend implements syncAIServiceBackendFn.
+func (c *AIBackendController) syncAIServiceBackend(ctx context.Context, aiBackend *aigv1a1.AIServiceBackend) error {
+	key := fmt.Sprintf("%s.%s", aiBackend.Name, aiBackend.Namespace)
+	var aiGatewayRoutes aigv1a1.AIGatewayRouteList
+	err := c.client.List(ctx, &aiGatewayRoutes, client.MatchingFields{k8sClientIndexBackendToReferencingAIGatewayRoute: key})
+	if err != nil {
+		return fmt.Errorf("failed to list AIGatewayRouteList: %w", err)
+	}
+	var errs []error
+	for _, aiGatewayRoute := range aiGatewayRoutes.Items {
+		c.logger.Info("syncing AIGatewayRoute",
+			"namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name,
+			"referenced_backend", aiBackend.Name, "referenced_backend_namespace", aiBackend.Namespace,
+		)
+		if err := c.syncRoute(ctx, &aiGatewayRoute); err != nil {
+			errs = append(errs, fmt.Errorf("%s: %w", aiGatewayRoute.Name, err))
+		}
+	}
+	if len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+	return nil
 }
diff --git a/internal/controller/backend_security_policy.go b/internal/controller/backend_security_policy.go
index c6652c1..b681c83 100644
--- a/internal/controller/backend_security_policy.go
+++ b/internal/controller/backend_security_policy.go
@@ -7,13 +7,14 @@ package controller
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/go-logr/logr"
 	"golang.org/x/oauth2"
-	"k8s.io/apimachinery/pkg/api/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/client-go/kubernetes"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -29,50 +30,49 @@ const preRotationWindow = 5 * time.Minute
 
 // backendSecurityPolicyController implements [reconcile.TypedReconciler] for [aigv1a1.BackendSecurityPolicy].
 //
-// This handles the BackendSecurityPolicy resource and sends it to the config sink so that it can modify configuration.
+// Exported for testing purposes.
 type backendSecurityPolicyController struct {
-	client         client.Client
-	kube           kubernetes.Interface
-	logger         logr.Logger
-	eventChan      chan ConfigSinkEvent
-	oidcTokenCache map[string]*oauth2.Token
+	client               client.Client
+	kube                 kubernetes.Interface
+	logger               logr.Logger
+	oidcTokenCache       map[string]*oauth2.Token
+	syncAIServiceBackend syncAIServiceBackendFn
 }
 
-func newBackendSecurityPolicyController(client client.Client, kube kubernetes.Interface, logger logr.Logger, ch chan ConfigSinkEvent) *backendSecurityPolicyController {
+func newBackendSecurityPolicyController(client client.Client, kube kubernetes.Interface, logger logr.Logger, syncAIServiceBackend syncAIServiceBackendFn) *backendSecurityPolicyController {
 	return &backendSecurityPolicyController{
-		client:         client,
-		kube:           kube,
-		logger:         logger,
-		eventChan:      ch,
-		oidcTokenCache: make(map[string]*oauth2.Token),
+		client:               client,
+		kube:                 kube,
+		logger:               logger,
+		oidcTokenCache:       make(map[string]*oauth2.Token),
+		syncAIServiceBackend: syncAIServiceBackend,
 	}
 }
 
 // Reconcile implements the [reconcile.TypedReconciler] for [aigv1a1.BackendSecurityPolicy].
-func (b *backendSecurityPolicyController) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
+func (c *backendSecurityPolicyController) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
 	var backendSecurityPolicy aigv1a1.BackendSecurityPolicy
-	if err = b.client.Get(ctx, req.NamespacedName, &backendSecurityPolicy); err != nil {
-		if errors.IsNotFound(err) {
-			ctrl.Log.Info("Deleting Backend Security Policy",
+	if err = c.client.Get(ctx, req.NamespacedName, &backendSecurityPolicy); err != nil {
+		if apierrors.IsNotFound(err) {
+			c.logger.Info("Deleting Backend Security Policy",
 				"namespace", req.Namespace, "name", req.Name)
 			return ctrl.Result{}, nil
 		}
 		return ctrl.Result{}, err
 	}
-
 	if oidc := getBackendSecurityPolicyAuthOIDC(backendSecurityPolicy.Spec); oidc != nil {
 		var rotator rotators.Rotator
 		switch backendSecurityPolicy.Spec.Type {
 		case aigv1a1.BackendSecurityPolicyTypeAWSCredentials:
 			region := backendSecurityPolicy.Spec.AWSCredentials.Region
 			roleArn := backendSecurityPolicy.Spec.AWSCredentials.OIDCExchangeToken.AwsRoleArn
-			rotator, err = rotators.NewAWSOIDCRotator(ctx, b.client, nil, b.kube, b.logger, backendSecurityPolicy.Namespace, backendSecurityPolicy.Name, preRotationWindow, roleArn, region)
+			rotator, err = rotators.NewAWSOIDCRotator(ctx, c.client, nil, c.kube, c.logger, backendSecurityPolicy.Namespace, backendSecurityPolicy.Name, preRotationWindow, roleArn, region)
 			if err != nil {
 				return ctrl.Result{}, err
 			}
 		default:
 			err = fmt.Errorf("backend security type %s does not support OIDC token exchange", backendSecurityPolicy.Spec.Type)
-			ctrl.Log.Error(err, "namespace", backendSecurityPolicy.Namespace, "name", backendSecurityPolicy.Name)
+			c.logger.Error(err, "namespace", backendSecurityPolicy.Namespace, "name", backendSecurityPolicy.Name)
 			return ctrl.Result{}, err
 		}
 
@@ -80,12 +80,12 @@ func (b *backendSecurityPolicyController) Reconcile(ctx context.Context, req ctr
 		var rotationTime time.Time
 		rotationTime, err = rotator.GetPreRotationTime(ctx)
 		if err != nil {
-			b.logger.Error(err, "failed to get rotation time, retry in one minute")
+			c.logger.Error(err, "failed to get rotation time, retry in one minute")
 		} else {
 			if rotator.IsExpired(rotationTime) {
-				requeue, err = b.rotateCredential(ctx, &backendSecurityPolicy, *oidc, rotator)
+				requeue, err = c.rotateCredential(ctx, &backendSecurityPolicy, *oidc, rotator)
 				if err != nil {
-					b.logger.Error(err, "failed to rotate OIDC exchange token, retry in one minute")
+					c.logger.Error(err, "failed to rotate OIDC exchange token, retry in one minute")
 				}
 			} else {
 				requeue = time.Until(rotationTime)
@@ -93,24 +93,22 @@ func (b *backendSecurityPolicyController) Reconcile(ctx context.Context, req ctr
 		}
 		res = ctrl.Result{RequeueAfter: requeue}
 	}
-	// Send the backend security policy to the config sink so that it can modify the configuration together with the state of other resources.
-	b.eventChan <- backendSecurityPolicy.DeepCopy()
-	return
+	return res, c.syncBackendSecurityPolicy(ctx, &backendSecurityPolicy)
 }
 
 // rotateCredential rotates the credentials using the access token from OIDC provider and return the requeue time for next rotation.
-func (b *backendSecurityPolicyController) rotateCredential(ctx context.Context, policy *aigv1a1.BackendSecurityPolicy, oidcCreds egv1a1.OIDC, rotator rotators.Rotator) (time.Duration, error) {
+func (c *backendSecurityPolicyController) rotateCredential(ctx context.Context, policy *aigv1a1.BackendSecurityPolicy, oidcCreds egv1a1.OIDC, rotator rotators.Rotator) (time.Duration, error) {
 	bspKey := backendSecurityPolicyKey(policy.Namespace, policy.Name)
 
 	var err error
-	validToken, ok := b.oidcTokenCache[bspKey]
+	validToken, ok := c.oidcTokenCache[bspKey]
 	if !ok || validToken == nil || rotators.IsBufferedTimeExpired(preRotationWindow, validToken.Expiry) {
-		oidcProvider := oauth.NewOIDCProvider(b.client, oidcCreds)
+		oidcProvider := oauth.NewOIDCProvider(c.client, oidcCreds)
 		validToken, err = oidcProvider.FetchToken(ctx)
 		if err != nil {
 			return time.Minute, err
 		}
-		b.oidcTokenCache[bspKey] = validToken
+		c.oidcTokenCache[bspKey] = validToken
 	}
 
 	token := validToken.AccessToken
@@ -143,3 +141,25 @@ func getBackendSecurityPolicyAuthOIDC(spec aigv1a1.BackendSecurityPolicySpec) *e
 func backendSecurityPolicyKey(namespace, name string) string {
 	return fmt.Sprintf("%s.%s", name, namespace)
 }
+
+func (c *backendSecurityPolicyController) syncBackendSecurityPolicy(ctx context.Context, bsp *aigv1a1.BackendSecurityPolicy) error {
+	key := backendSecurityPolicyKey(bsp.Namespace, bsp.Name)
+	var aiServiceBackends aigv1a1.AIServiceBackendList
+	err := c.client.List(ctx, &aiServiceBackends, client.MatchingFields{k8sClientIndexBackendSecurityPolicyToReferencingAIServiceBackend: key})
+	if err != nil {
+		return fmt.Errorf("failed to list AIServiceBackendList: %w", err)
+	}
+
+	var errs []error
+	for i := range aiServiceBackends.Items {
+		aiBackend := &aiServiceBackends.Items[i]
+		c.logger.Info("Syncing AIServiceBackend", "namespace", aiBackend.Namespace, "name", aiBackend.Name)
+		if err = c.syncAIServiceBackend(ctx, aiBackend); err != nil {
+			errs = append(errs, fmt.Errorf("%s/%s: %w", aiBackend.Namespace, aiBackend.Name, err))
+		}
+	}
+	if len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+	return nil
+}
diff --git a/internal/controller/controller.go b/internal/controller/controller.go
index c8d189a..e73a579 100644
--- a/internal/controller/controller.go
+++ b/internal/controller/controller.go
@@ -50,6 +50,18 @@ type Options struct {
 	EnableLeaderElection bool
 }
 
+type (
+	// syncAIGatewayRouteFn is a function that syncs an AIGatewayRoute. This is used to cross the controller boundary
+	// from AIServiceBackend to AIGatewayRoute when an AIServiceBackend is referenced by an AIGatewayRoute.
+	syncAIGatewayRouteFn func(context.Context, *aigv1a1.AIGatewayRoute) error
+	// syncAIServiceBackendFn is a function that syncs an AIServiceBackend. This is used to cross the controller boundary
+	// from BackendSecurityPolicy to AIServiceBackend when a BackendSecurityPolicy is referenced by an AIServiceBackend.
+	syncAIServiceBackendFn func(context.Context, *aigv1a1.AIServiceBackend) error
+	// syncBackendSecurityPolicyFn is a function that syncs a BackendSecurityPolicy. This is used to cross the controller boundary
+	// from Secret to BackendSecurityPolicy when a Secret is referenced by a BackendSecurityPolicy.
+	syncBackendSecurityPolicyFn func(context.Context, *aigv1a1.BackendSecurityPolicy) error
+)
+
 // StartControllers starts the controllers for the AI Gateway.
 // This blocks until the manager is stopped.
 //
@@ -68,13 +80,12 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg
 
 	c := mgr.GetClient()
 	indexer := mgr.GetFieldIndexer()
-	if err = applyIndexing(ctx, indexer.IndexField); err != nil {
+	if err = ApplyIndexing(ctx, indexer.IndexField); err != nil {
 		return fmt.Errorf("failed to apply indexing: %w", err)
 	}
 
-	sinkChan := make(chan ConfigSinkEvent, 100)
-	routeC := NewAIGatewayRouteController(c, kubernetes.NewForConfigOrDie(config), logger.
-		WithName("ai-gateway-route"), sinkChan)
+	routeC := NewAIGatewayRouteController(c, kubernetes.NewForConfigOrDie(config), logger.WithName("ai-gateway-route"),
+		options.ExtProcImage, options.ExtProcLogLevel)
 	if err = ctrl.NewControllerManagedBy(mgr).
 		For(&aigv1a1.AIGatewayRoute{}).
 		Owns(&egv1a1.EnvoyExtensionPolicy{}).
@@ -86,7 +97,7 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg
 	}
 
 	backendC := NewAIServiceBackendController(c, kubernetes.NewForConfigOrDie(config), logger.
-		WithName("ai-service-backend"), sinkChan)
+		WithName("ai-service-backend"), routeC.syncAIGatewayRoute)
 	if err = ctrl.NewControllerManagedBy(mgr).
 		For(&aigv1a1.AIServiceBackend{}).
 		Complete(backendC); err != nil {
@@ -94,7 +105,7 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg
 	}
 
 	backendSecurityPolicyC := newBackendSecurityPolicyController(c, kubernetes.NewForConfigOrDie(config), logger.
-		WithName("backend-security-policy"), sinkChan)
+		WithName("backend-security-policy"), backendC.syncAIServiceBackend)
 	if err = ctrl.NewControllerManagedBy(mgr).
 		For(&aigv1a1.BackendSecurityPolicy{}).
 		Complete(backendSecurityPolicyC); err != nil {
@@ -102,21 +113,13 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg
 	}
 
 	secretC := NewSecretController(c, kubernetes.NewForConfigOrDie(config), logger.
-		WithName("secret"), sinkChan)
+		WithName("secret"), backendSecurityPolicyC.syncBackendSecurityPolicy)
 	if err = ctrl.NewControllerManagedBy(mgr).
 		For(&corev1.Secret{}).
 		Complete(secretC); err != nil {
 		return fmt.Errorf("failed to create controller for Secret: %w", err)
 	}
 
-	sink := newConfigSink(c, kubernetes.NewForConfigOrDie(config), logger.
-		WithName("config-sink"), sinkChan, options.ExtProcImage, options.ExtProcLogLevel)
-
-	// Before starting the manager, initialize the config sink to sync all AIServiceBackend and AIGatewayRoute objects in the cluster.
-	if err = sink.init(ctx); err != nil {
-		return fmt.Errorf("failed to initialize config sink: %w", err)
-	}
-
 	if err = mgr.Start(ctx); err != nil { // This blocks until the manager is stopped.
 		return fmt.Errorf("failed to start controller manager: %w", err)
 	}
@@ -135,7 +138,8 @@ const (
 	k8sClientIndexBackendSecurityPolicyToReferencingAIServiceBackend = "BackendSecurityPolicyToReferencingAIServiceBackend"
 )
 
-func applyIndexing(ctx context.Context, indexer func(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error) error {
+// ApplyIndexing applies indexing to the given indexer. This is exported for testing purposes.
+func ApplyIndexing(ctx context.Context, indexer func(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error) error {
 	err := indexer(ctx, &aigv1a1.AIGatewayRoute{},
 		k8sClientIndexBackendToReferencingAIGatewayRoute, aiGatewayRouteIndexFunc)
 	if err != nil {
diff --git a/internal/controller/rotators/aws_oidc_rotator.go b/internal/controller/rotators/aws_oidc_rotator.go
index 9db3b65..9148454 100644
--- a/internal/controller/rotators/aws_oidc_rotator.go
+++ b/internal/controller/rotators/aws_oidc_rotator.go
@@ -17,7 +17,7 @@ import (
 	"github.com/aws/aws-sdk-go-v2/service/sts"
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -103,7 +103,7 @@ func (r *AWSOIDCRotator) GetPreRotationTime(ctx context.Context) (time.Time, err
 	secret, err := LookupSecret(ctx, r.client, r.backendSecurityPolicyNamespace, GetBSPSecretName(r.backendSecurityPolicyName))
 	if err != nil {
 		// return zero value for time if secret has not been created.
-		if errors.IsNotFound(err) {
+		if apierrors.IsNotFound(err) {
 			return time.Time{}, nil
 		}
 		return time.Time{}, err
@@ -132,7 +132,7 @@ func (r *AWSOIDCRotator) Rotate(ctx context.Context, token string) error {
 
 	secret, err := LookupSecret(ctx, r.client, r.backendSecurityPolicyNamespace, GetBSPSecretName(r.backendSecurityPolicyName))
 	if err != nil {
-		if !errors.IsNotFound(err) {
+		if !apierrors.IsNotFound(err) {
 			return err
 		}
 		secret = &corev1.Secret{
@@ -161,7 +161,7 @@ func (r *AWSOIDCRotator) Rotate(ctx context.Context, token string) error {
 
 	err = r.client.Create(ctx, secret)
 	if err != nil {
-		if !errors.IsAlreadyExists(err) {
+		if !apierrors.IsAlreadyExists(err) {
 			return r.client.Update(ctx, secret)
 		}
 		return fmt.Errorf("failed to create secret: %w", err)
diff --git a/internal/controller/rotators/common.go b/internal/controller/rotators/common.go
index 4376bdb..c43049a 100644
--- a/internal/controller/rotators/common.go
+++ b/internal/controller/rotators/common.go
@@ -11,7 +11,7 @@ import (
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -37,7 +37,7 @@ func LookupSecret(ctx context.Context, k8sClient client.Client, namespace, name
 		Namespace: namespace,
 		Name:      name,
 	}, secret); err != nil {
-		if errors.IsNotFound(err) {
+		if apierrors.IsNotFound(err) {
 			return nil, err
 		}
 		return nil, fmt.Errorf("failed to get secret: %w", err)
diff --git a/internal/controller/secret.go b/internal/controller/secret.go
index 510b995..8fc90bb 100644
--- a/internal/controller/secret.go
+++ b/internal/controller/secret.go
@@ -7,6 +7,8 @@ package controller
 
 import (
 	"context"
+	"errors"
+	"fmt"
 
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
@@ -15,38 +17,68 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
 )
 
 // secretController implements reconcile.TypedReconciler for corev1.Secret.
 type secretController struct {
-	client     client.Client
-	kubeClient kubernetes.Interface
-	logger     logr.Logger
-	eventChan  chan ConfigSinkEvent
+	client                    client.Client
+	kubeClient                kubernetes.Interface
+	logger                    logr.Logger
+	syncBackendSecurityPolicy syncBackendSecurityPolicyFn
 }
 
 // NewSecretController creates a new reconcile.TypedReconciler[reconcile.Request] for corev1.Secret.
 func NewSecretController(client client.Client, kubeClient kubernetes.Interface,
-	logger logr.Logger, ch chan ConfigSinkEvent,
+	logger logr.Logger, syncBackendSecurityPolicy syncBackendSecurityPolicyFn,
 ) reconcile.TypedReconciler[reconcile.Request] {
 	return &secretController{
-		client:     client,
-		kubeClient: kubeClient,
-		logger:     logger,
-		eventChan:  ch,
+		client:                    client,
+		kubeClient:                kubeClient,
+		logger:                    logger,
+		syncBackendSecurityPolicy: syncBackendSecurityPolicy,
 	}
 }
 
 // Reconcile implements the reconcile.Reconciler for corev1.Secret.
-func (r *secretController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+func (c *secretController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	var secret corev1.Secret
-	if err := r.client.Get(ctx, req.NamespacedName, &secret); err != nil {
+	if err := c.client.Get(ctx, req.NamespacedName, &secret); err != nil {
 		if apierrors.IsNotFound(err) {
 			return ctrl.Result{}, nil
 		}
 		return ctrl.Result{}, err
 	}
-	r.logger.Info("Reconciling Secret", "namespace", req.Namespace, "name", req.Name)
-	r.eventChan <- ConfigSinkEventSecretUpdate{Namespace: secret.Namespace, Name: secret.Name}
+	c.logger.Info("Reconciling Secret", "namespace", req.Namespace, "name", req.Name)
+	if err := c.syncSecret(ctx, req.Namespace, req.Name); err != nil {
+		return ctrl.Result{}, err
+	}
 	return ctrl.Result{}, nil
 }
+
+// syncSecret syncs the state of all resource referencing the given secret.
+func (c *secretController) syncSecret(ctx context.Context, namespace, name string) error {
+	var backendSecurityPolicies aigv1a1.BackendSecurityPolicyList
+	err := c.client.List(ctx, &backendSecurityPolicies,
+		client.MatchingFields{
+			k8sClientIndexSecretToReferencingBackendSecurityPolicy: backendSecurityPolicyKey(namespace, name),
+		},
+	)
+	if err != nil {
+		return fmt.Errorf("failed to list BackendSecurityPolicyList: %w", err)
+	}
+	var errs []error
+	for i := range backendSecurityPolicies.Items {
+		backendSecurityPolicy := &backendSecurityPolicies.Items[i]
+		c.logger.Info("Syncing BackendSecurityPolicy",
+			"namespace", backendSecurityPolicy.Namespace, "name", backendSecurityPolicy.Name)
+		if err = c.syncBackendSecurityPolicy(ctx, backendSecurityPolicy); err != nil {
+			errs = append(errs, fmt.Errorf("%s: %w", backendSecurityPolicy.Name, err))
+		}
+	}
+	if len(errs) > 0 {
+		return errors.Join(errs...)
+	}
+	return nil
+}
diff --git a/internal/controller/sink.go b/internal/controller/sink.go
deleted file mode 100644
index c583d39..0000000
--- a/internal/controller/sink.go
+++ /dev/null
@@ -1,661 +0,0 @@
-// Copyright Envoy AI Gateway Authors
-// SPDX-License-Identifier: Apache-2.0
-// The full text of the Apache license is available in the LICENSE file at
-// the root of the repo.
-
-package controller
-
-import (
-	"context"
-	"fmt"
-	"path"
-
-	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
-	"github.com/go-logr/logr"
-	appsv1 "k8s.io/api/apps/v1"
-	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/types"
-	uuid2 "k8s.io/apimachinery/pkg/util/uuid"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/utils/ptr"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
-	"sigs.k8s.io/yaml"
-
-	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
-	"github.com/envoyproxy/ai-gateway/filterapi"
-	"github.com/envoyproxy/ai-gateway/internal/controller/rotators"
-	"github.com/envoyproxy/ai-gateway/internal/llmcostcel"
-)
-
-const (
-	selectedBackendHeaderKey   = "x-ai-eg-selected-backend"
-	hostRewriteHTTPFilterName  = "ai-eg-host-rewrite"
-	extProcConfigAnnotationKey = "aigateway.envoyproxy.io/extproc-config-uuid"
-)
-
-// mountedExtProcSecretPath specifies the secret file mounted on the external proc. The idea is to update the mounted
-//
-//	secret with backendSecurityPolicy auth instead of mounting new secret files to the external proc.
-const mountedExtProcSecretPath = "/etc/backend_security_policy" // #nosec G101
-
-// ConfigSinkEvent is the interface for the events that the configSink can handle.
-// It can be either an AIServiceBackend, an AIGatewayRoute, or a deletion event.
-//
-// Exported for internal testing purposes.
-type ConfigSinkEvent any
-
-// ConfigSinkEventSecretUpdate is an event that indicates that a secret has been updated.
-// It only contains the namespace and the name of the secret that has been updated.
-type ConfigSinkEventSecretUpdate struct {
-	Namespace, Name string
-}
-
-// configSink centralizes the AIGatewayRoute and AIServiceBackend objects handling
-// which requires to be done in a single goroutine since we need to
-// consolidate the information from both objects to generate the ExtProc ConfigMap
-// and HTTPRoute objects.
-type configSink struct {
-	client                 client.Client
-	kube                   kubernetes.Interface
-	logger                 logr.Logger
-	extProcImage           string
-	extProcImagePullPolicy corev1.PullPolicy
-	extProcLogLevel        string
-	eventChan              chan ConfigSinkEvent
-}
-
-func newConfigSink(
-	kubeClient client.Client,
-	kube kubernetes.Interface,
-	logger logr.Logger,
-	eventChan chan ConfigSinkEvent,
-	extProcImage string,
-	extProcLogLevel string,
-) *configSink {
-	c := &configSink{
-		client:                 kubeClient,
-		kube:                   kube,
-		logger:                 logger,
-		extProcImage:           extProcImage,
-		extProcImagePullPolicy: corev1.PullIfNotPresent,
-		extProcLogLevel:        extProcLogLevel,
-		eventChan:              eventChan,
-	}
-	return c
-}
-
-func (c *configSink) backend(ctx context.Context, namespace, name string) (*aigv1a1.AIServiceBackend, error) {
-	backend := &aigv1a1.AIServiceBackend{}
-	if err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, backend); err != nil {
-		return nil, err
-	}
-	return backend, nil
-}
-
-func (c *configSink) backendSecurityPolicy(ctx context.Context, namespace, name string) (*aigv1a1.BackendSecurityPolicy, error) {
-	backendSecurityPolicy := &aigv1a1.BackendSecurityPolicy{}
-	if err := c.client.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, backendSecurityPolicy); err != nil {
-		return nil, err
-	}
-	return backendSecurityPolicy, nil
-}
-
-// init starts a goroutine to handle the events from the controllers.
-func (c *configSink) init(ctx context.Context) error {
-	go func() {
-		for {
-			select {
-			case <-ctx.Done():
-				close(c.eventChan)
-				return
-			case event := <-c.eventChan:
-				c.handleEvent(ctx, event)
-			}
-		}
-	}()
-	return nil
-}
-
-// handleEvent handles the event received from the controllers in a single goroutine.
-func (c *configSink) handleEvent(ctx context.Context, event ConfigSinkEvent) {
-	switch e := event.(type) {
-	case *aigv1a1.AIServiceBackend:
-		c.syncAIServiceBackend(ctx, e)
-	case *aigv1a1.AIGatewayRoute:
-		c.syncAIGatewayRoute(ctx, e)
-	case *aigv1a1.BackendSecurityPolicy:
-		c.syncBackendSecurityPolicy(ctx, e)
-	case ConfigSinkEventSecretUpdate:
-		c.syncSecret(ctx, e.Namespace, e.Name)
-	default:
-		panic(fmt.Sprintf("unexpected event type: %T", e))
-	}
-}
-
-func (c *configSink) syncAIGatewayRoute(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) {
-	// Check if the HTTPRouteFilter exists in the namespace.
-	var httpRouteFilter egv1a1.HTTPRouteFilter
-	err := c.client.Get(ctx,
-		client.ObjectKey{Name: hostRewriteHTTPFilterName, Namespace: aiGatewayRoute.Namespace}, &httpRouteFilter)
-	if apierrors.IsNotFound(err) {
-		httpRouteFilter = egv1a1.HTTPRouteFilter{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      hostRewriteHTTPFilterName,
-				Namespace: aiGatewayRoute.Namespace,
-			},
-			Spec: egv1a1.HTTPRouteFilterSpec{
-				URLRewrite: &egv1a1.HTTPURLRewriteFilter{
-					Hostname: &egv1a1.HTTPHostnameModifier{
-						Type: egv1a1.BackendHTTPHostnameModifier,
-					},
-				},
-			},
-		}
-		if err = c.client.Create(ctx, &httpRouteFilter); err != nil {
-			c.logger.Error(err, "failed to create HTTPRouteFilter", "namespace", aiGatewayRoute.Namespace, "name", hostRewriteHTTPFilterName)
-			return
-		}
-	} else if err != nil {
-		c.logger.Error(err, "failed to get HTTPRouteFilter", "namespace", aiGatewayRoute.Namespace, "name", hostRewriteHTTPFilterName, "error", err)
-		return
-	}
-
-	// Check if the HTTPRoute exists.
-	c.logger.Info("syncing AIGatewayRoute", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
-	var httpRoute gwapiv1.HTTPRoute
-	err = c.client.Get(ctx, client.ObjectKey{Name: aiGatewayRoute.Name, Namespace: aiGatewayRoute.Namespace}, &httpRoute)
-	existingRoute := err == nil
-	if apierrors.IsNotFound(err) {
-		// This means that this AIGatewayRoute is a new one.
-		httpRoute = gwapiv1.HTTPRoute{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      aiGatewayRoute.Name,
-				Namespace: aiGatewayRoute.Namespace,
-			},
-			Spec: gwapiv1.HTTPRouteSpec{},
-		}
-		if err = ctrlutil.SetControllerReference(aiGatewayRoute, &httpRoute, c.client.Scheme()); err != nil {
-			panic(fmt.Errorf("BUG: failed to set controller reference for HTTPRoute: %w", err))
-		}
-	} else if err != nil {
-		c.logger.Error(err, "failed to get HTTPRoute", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name, "error", err)
-		return
-	}
-
-	// Update the HTTPRoute with the new AIGatewayRoute.
-	if err = c.newHTTPRoute(ctx, &httpRoute, aiGatewayRoute); err != nil {
-		c.logger.Error(err, "failed to update HTTPRoute with AIGatewayRoute", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
-		return
-	}
-
-	if existingRoute {
-		c.logger.Info("updating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
-		if err = c.client.Update(ctx, &httpRoute); err != nil {
-			c.logger.Error(err, "failed to update HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
-			return
-		}
-	} else {
-		c.logger.Info("creating HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
-		if err = c.client.Create(ctx, &httpRoute); err != nil {
-			c.logger.Error(err, "failed to create HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name)
-			return
-		}
-	}
-
-	// Update the extproc configmap.
-	uuid := string(uuid2.NewUUID())
-	if err = c.updateExtProcConfigMap(ctx, aiGatewayRoute, uuid); err != nil {
-		c.logger.Error(err, "failed to update extproc configmap", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
-		return
-	}
-
-	// Deploy extproc deployment with potential updates.
-	err = c.syncExtProcDeployment(ctx, aiGatewayRoute)
-	if err != nil {
-		c.logger.Error(err, "failed to deploy ext proc", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
-		return
-	}
-
-	// Annotate all pods with the new config.
-	err = c.annotateExtProcPods(ctx, aiGatewayRoute, uuid)
-	if err != nil {
-		c.logger.Error(err, "failed to annotate pods", "namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name)
-		return
-	}
-}
-
-func (c *configSink) syncAIServiceBackend(ctx context.Context, aiBackend *aigv1a1.AIServiceBackend) {
-	key := fmt.Sprintf("%s.%s", aiBackend.Name, aiBackend.Namespace)
-	var aiGatewayRoutes aigv1a1.AIGatewayRouteList
-	err := c.client.List(ctx, &aiGatewayRoutes, client.MatchingFields{k8sClientIndexBackendToReferencingAIGatewayRoute: key})
-	if err != nil {
-		c.logger.Error(err, "failed to list AIGatewayRoute", "backend", key)
-		return
-	}
-	for _, aiGatewayRoute := range aiGatewayRoutes.Items {
-		c.logger.Info("syncing AIGatewayRoute",
-			"namespace", aiGatewayRoute.Namespace, "name", aiGatewayRoute.Name,
-			"referenced_backend", aiBackend.Name, "referenced_backend_namespace", aiBackend.Namespace,
-		)
-		c.syncAIGatewayRoute(ctx, &aiGatewayRoute)
-	}
-}
-
-func (c *configSink) syncBackendSecurityPolicy(ctx context.Context, bsp *aigv1a1.BackendSecurityPolicy) {
-	key := backendSecurityPolicyKey(bsp.Namespace, bsp.Name)
-	var aiServiceBackends aigv1a1.AIServiceBackendList
-	err := c.client.List(ctx, &aiServiceBackends, client.MatchingFields{k8sClientIndexBackendSecurityPolicyToReferencingAIServiceBackend: key})
-	if err != nil {
-		c.logger.Error(err, "failed to list AIServiceBackendList", "backendSecurityPolicy", key)
-		return
-	}
-	for i := range aiServiceBackends.Items {
-		aiBackend := &aiServiceBackends.Items[i]
-		c.syncAIServiceBackend(ctx, aiBackend)
-	}
-}
-
-// updateExtProcConfigMap updates the external process configmap with the new AIGatewayRoute.
-func (c *configSink) updateExtProcConfigMap(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute, uuid string) error {
-	configMap, err := c.kube.CoreV1().ConfigMaps(aiGatewayRoute.Namespace).Get(ctx, extProcName(aiGatewayRoute), metav1.GetOptions{})
-	if err != nil {
-		// This is a bug since we should have created the configmap before sending the AIGatewayRoute to the configSink.
-		panic(fmt.Errorf("failed to get configmap %s: %w", extProcName(aiGatewayRoute), err))
-	}
-
-	ec := &filterapi.Config{UUID: uuid}
-	spec := &aiGatewayRoute.Spec
-
-	ec.Schema.Name = filterapi.APISchemaName(spec.APISchema.Name)
-	ec.Schema.Version = spec.APISchema.Version
-	ec.ModelNameHeaderKey = aigv1a1.AIModelHeaderKey
-	ec.SelectedBackendHeaderKey = selectedBackendHeaderKey
-	ec.Rules = make([]filterapi.RouteRule, len(spec.Rules))
-	for i := range spec.Rules {
-		rule := &spec.Rules[i]
-		ec.Rules[i].Backends = make([]filterapi.Backend, len(rule.BackendRefs))
-		for j := range rule.BackendRefs {
-			backend := &rule.BackendRefs[j]
-			key := fmt.Sprintf("%s.%s", backend.Name, aiGatewayRoute.Namespace)
-			ec.Rules[i].Backends[j].Name = key
-			ec.Rules[i].Backends[j].Weight = backend.Weight
-			var backendObj *aigv1a1.AIServiceBackend
-			backendObj, err = c.backend(ctx, aiGatewayRoute.Namespace, backend.Name)
-			if err != nil {
-				return fmt.Errorf("failed to get AIServiceBackend %s: %w", key, err)
-			}
-			ec.Rules[i].Backends[j].Schema.Name = filterapi.APISchemaName(backendObj.Spec.APISchema.Name)
-			ec.Rules[i].Backends[j].Schema.Version = backendObj.Spec.APISchema.Version
-
-			if bspRef := backendObj.Spec.BackendSecurityPolicyRef; bspRef != nil {
-				volumeName := backendSecurityPolicyVolumeName(
-					i, j, string(backendObj.Spec.BackendSecurityPolicyRef.Name),
-				)
-				var backendSecurityPolicy *aigv1a1.BackendSecurityPolicy
-				backendSecurityPolicy, err = c.backendSecurityPolicy(ctx, aiGatewayRoute.Namespace, string(bspRef.Name))
-				if err != nil {
-					return fmt.Errorf("failed to get BackendSecurityPolicy %s: %w", bspRef.Name, err)
-				}
-
-				switch backendSecurityPolicy.Spec.Type {
-				case aigv1a1.BackendSecurityPolicyTypeAPIKey:
-					ec.Rules[i].Backends[j].Auth = &filterapi.BackendAuth{
-						APIKey: &filterapi.APIKeyAuth{Filename: path.Join(backendSecurityMountPath(volumeName), "/apiKey")},
-					}
-				case aigv1a1.BackendSecurityPolicyTypeAWSCredentials:
-					if backendSecurityPolicy.Spec.AWSCredentials == nil {
-						return fmt.Errorf("AWSCredentials type selected but not defined %s", backendSecurityPolicy.Name)
-					}
-					if awsCred := backendSecurityPolicy.Spec.AWSCredentials; awsCred.CredentialsFile != nil || awsCred.OIDCExchangeToken != nil {
-						ec.Rules[i].Backends[j].Auth = &filterapi.BackendAuth{
-							AWSAuth: &filterapi.AWSAuth{
-								CredentialFileName: path.Join(backendSecurityMountPath(volumeName), "/credentials"),
-								Region:             backendSecurityPolicy.Spec.AWSCredentials.Region,
-							},
-						}
-					}
-				default:
-					return fmt.Errorf("invalid backend security type %s for policy %s", backendSecurityPolicy.Spec.Type,
-						backendSecurityPolicy.Name)
-				}
-			}
-		}
-		ec.Rules[i].Headers = make([]filterapi.HeaderMatch, len(rule.Matches))
-		for j, match := range rule.Matches {
-			ec.Rules[i].Headers[j].Name = match.Headers[0].Name
-			ec.Rules[i].Headers[j].Value = match.Headers[0].Value
-		}
-	}
-
-	ec.MetadataNamespace = aigv1a1.AIGatewayFilterMetadataNamespace
-	for _, cost := range aiGatewayRoute.Spec.LLMRequestCosts {
-		fc := filterapi.LLMRequestCost{MetadataKey: cost.MetadataKey}
-		switch cost.Type {
-		case aigv1a1.LLMRequestCostTypeInputToken:
-			fc.Type = filterapi.LLMRequestCostTypeInputToken
-		case aigv1a1.LLMRequestCostTypeOutputToken:
-			fc.Type = filterapi.LLMRequestCostTypeOutputToken
-		case aigv1a1.LLMRequestCostTypeTotalToken:
-			fc.Type = filterapi.LLMRequestCostTypeTotalToken
-		case aigv1a1.LLMRequestCostTypeCEL:
-			fc.Type = filterapi.LLMRequestCostTypeCELExpression
-			expr := *cost.CELExpression
-			// Sanity check the CEL expression.
-			_, err = llmcostcel.NewProgram(expr)
-			if err != nil {
-				return fmt.Errorf("invalid CEL expression: %w", err)
-			}
-			fc.CELExpression = expr
-		default:
-			return fmt.Errorf("unknown request cost type: %s", cost.Type)
-		}
-		ec.LLMRequestCosts = append(ec.LLMRequestCosts, fc)
-	}
-
-	marshaled, err := yaml.Marshal(ec)
-	if err != nil {
-		return fmt.Errorf("failed to marshal extproc config: %w", err)
-	}
-	if configMap.Data == nil {
-		configMap.Data = make(map[string]string)
-	}
-	configMap.Data[expProcConfigFileName] = string(marshaled)
-	if _, err := c.kube.CoreV1().ConfigMaps(aiGatewayRoute.Namespace).Update(ctx, configMap, metav1.UpdateOptions{}); err != nil {
-		return fmt.Errorf("failed to update configmap %s: %w", configMap.Name, err)
-	}
-	return nil
-}
-
-// newHTTPRoute updates the HTTPRoute with the new AIGatewayRoute.
-func (c *configSink) newHTTPRoute(ctx context.Context, dst *gwapiv1.HTTPRoute, aiGatewayRoute *aigv1a1.AIGatewayRoute) error {
-	var backends []*aigv1a1.AIServiceBackend
-	dedup := make(map[string]struct{})
-	for _, rule := range aiGatewayRoute.Spec.Rules {
-		for _, br := range rule.BackendRefs {
-			key := fmt.Sprintf("%s.%s", br.Name, aiGatewayRoute.Namespace)
-			if _, ok := dedup[key]; ok {
-				continue
-			}
-			dedup[key] = struct{}{}
-			backend, err := c.backend(ctx, aiGatewayRoute.Namespace, br.Name)
-			if err != nil {
-				return fmt.Errorf("AIServiceBackend %s not found", key)
-			}
-			backends = append(backends, backend)
-		}
-	}
-
-	rewriteFilters := []gwapiv1.HTTPRouteFilter{
-		{
-			Type: gwapiv1.HTTPRouteFilterExtensionRef,
-			ExtensionRef: &gwapiv1.LocalObjectReference{
-				Group: "gateway.envoyproxy.io",
-				Kind:  "HTTPRouteFilter",
-				Name:  hostRewriteHTTPFilterName,
-			},
-		},
-	}
-	rules := make([]gwapiv1.HTTPRouteRule, len(backends))
-	for i, b := range backends {
-		key := fmt.Sprintf("%s.%s", b.Name, b.Namespace)
-		rule := gwapiv1.HTTPRouteRule{
-			BackendRefs: []gwapiv1.HTTPBackendRef{
-				{BackendRef: gwapiv1.BackendRef{BackendObjectReference: b.Spec.BackendRef}},
-			},
-			Matches: []gwapiv1.HTTPRouteMatch{
-				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: key}}},
-			},
-			Filters: rewriteFilters,
-		}
-		rules[i] = rule
-	}
-
-	// Adds the default route rule with "/" path.
-	if len(rules) > 0 {
-		rules = append(rules, gwapiv1.HTTPRouteRule{
-			Matches: []gwapiv1.HTTPRouteMatch{
-				{Path: &gwapiv1.HTTPPathMatch{Value: ptr.To("/")}},
-			},
-			BackendRefs: []gwapiv1.HTTPBackendRef{
-				{BackendRef: gwapiv1.BackendRef{BackendObjectReference: backends[0].Spec.BackendRef}},
-			},
-			Filters: rewriteFilters,
-		})
-	}
-
-	dst.Spec.Rules = rules
-
-	targetRefs := aiGatewayRoute.Spec.TargetRefs
-	egNs := gwapiv1.Namespace(aiGatewayRoute.Namespace)
-	parentRefs := make([]gwapiv1.ParentReference, len(targetRefs))
-	for i, egRef := range targetRefs {
-		egName := egRef.Name
-		parentRefs[i] = gwapiv1.ParentReference{
-			Name:      egName,
-			Namespace: &egNs,
-		}
-	}
-	dst.Spec.CommonRouteSpec.ParentRefs = parentRefs
-	return nil
-}
-
-// annotateExtProcPods annotates the external processor pods with the new config uuid.
-// This is necessary to make the config update faster.
-//
-// See https://neonmirrors.net/post/2022-12/reducing-pod-volume-update-times/ for explanation.
-func (c *configSink) annotateExtProcPods(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute, uuid string) error {
-	pods, err := c.kube.CoreV1().Pods(aiGatewayRoute.Namespace).List(ctx, metav1.ListOptions{
-		LabelSelector: fmt.Sprintf("app=%s", extProcName(aiGatewayRoute)),
-	})
-	if err != nil {
-		return fmt.Errorf("failed to list pods: %w", err)
-	}
-
-	for _, pod := range pods.Items {
-		c.logger.Info("annotating pod", "namespace", pod.Namespace, "name", pod.Name)
-		_, err = c.kube.CoreV1().Pods(pod.Namespace).Patch(ctx, pod.Name, types.MergePatchType,
-			[]byte(fmt.Sprintf(
-				`{"metadata":{"annotations":{"%s":"%s"}}}`, extProcConfigAnnotationKey, uuid),
-			), metav1.PatchOptions{})
-		if err != nil {
-			return fmt.Errorf("failed to patch pod %s: %w", pod.Name, err)
-		}
-	}
-	return nil
-}
-
-// syncExtProcDeployment syncs the external processor's Deployment and Service.
-func (c *configSink) syncExtProcDeployment(ctx context.Context, aiGatewayRoute *aigv1a1.AIGatewayRoute) error {
-	name := extProcName(aiGatewayRoute)
-	labels := map[string]string{"app": name, managedByLabel: "envoy-ai-gateway"}
-
-	deployment, err := c.kube.AppsV1().Deployments(aiGatewayRoute.Namespace).Get(ctx, extProcName(aiGatewayRoute), metav1.GetOptions{})
-	if err != nil {
-		if client.IgnoreNotFound(err) == nil {
-			deployment = &appsv1.Deployment{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      name,
-					Namespace: aiGatewayRoute.Namespace,
-					Labels:    labels,
-				},
-				Spec: appsv1.DeploymentSpec{
-					Selector: &metav1.LabelSelector{MatchLabels: labels},
-					Template: corev1.PodTemplateSpec{
-						ObjectMeta: metav1.ObjectMeta{Labels: labels},
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Name:            name,
-									Image:           c.extProcImage,
-									ImagePullPolicy: c.extProcImagePullPolicy,
-									Ports:           []corev1.ContainerPort{{Name: "grpc", ContainerPort: 1063}},
-									Args: []string{
-										"-configPath", "/etc/ai-gateway/extproc/" + expProcConfigFileName,
-										"-logLevel", c.extProcLogLevel,
-									},
-									VolumeMounts: []corev1.VolumeMount{
-										{
-											Name:      "config",
-											MountPath: "/etc/ai-gateway/extproc",
-											ReadOnly:  true,
-										},
-									},
-								},
-							},
-							Volumes: []corev1.Volume{
-								{
-									Name: "config",
-									VolumeSource: corev1.VolumeSource{
-										ConfigMap: &corev1.ConfigMapVolumeSource{
-											LocalObjectReference: corev1.LocalObjectReference{Name: extProcName(aiGatewayRoute)},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			}
-			if err = ctrlutil.SetControllerReference(aiGatewayRoute, deployment, c.client.Scheme()); err != nil {
-				panic(fmt.Errorf("BUG: failed to set controller reference for deployment: %w", err))
-			}
-			var updatedSpec *corev1.PodSpec
-			updatedSpec, err = c.mountBackendSecurityPolicySecrets(ctx, &deployment.Spec.Template.Spec, aiGatewayRoute)
-			if err == nil {
-				deployment.Spec.Template.Spec = *updatedSpec
-			}
-			applyExtProcDeploymentConfigUpdate(&deployment.Spec, aiGatewayRoute.Spec.FilterConfig)
-			_, err = c.kube.AppsV1().Deployments(aiGatewayRoute.Namespace).Create(ctx, deployment, metav1.CreateOptions{})
-			if err != nil {
-				return fmt.Errorf("failed to create deployment: %w", err)
-			}
-			c.logger.Info("Created deployment", "name", name)
-		} else {
-			return fmt.Errorf("failed to get deployment: %w", err)
-		}
-	} else {
-		var updatedSpec *corev1.PodSpec
-		updatedSpec, err = c.mountBackendSecurityPolicySecrets(ctx, &deployment.Spec.Template.Spec, aiGatewayRoute)
-		if err == nil {
-			deployment.Spec.Template.Spec = *updatedSpec
-		}
-		applyExtProcDeploymentConfigUpdate(&deployment.Spec, aiGatewayRoute.Spec.FilterConfig)
-		if _, err = c.kube.AppsV1().Deployments(aiGatewayRoute.Namespace).Update(ctx, deployment, metav1.UpdateOptions{}); err != nil {
-			return fmt.Errorf("failed to update deployment: %w", err)
-		}
-	}
-
-	// This is static, so we don't need to update it.
-	service := &corev1.Service{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      name,
-			Namespace: aiGatewayRoute.Namespace,
-			Labels:    labels,
-		},
-		Spec: corev1.ServiceSpec{
-			Selector: labels,
-			Ports: []corev1.ServicePort{
-				{
-					Name:        "grpc",
-					Protocol:    corev1.ProtocolTCP,
-					Port:        1063,
-					AppProtocol: ptr.To("grpc"),
-				},
-			},
-		},
-	}
-	if err = ctrlutil.SetControllerReference(aiGatewayRoute, service, c.client.Scheme()); err != nil {
-		panic(fmt.Errorf("BUG: failed to set controller reference for service: %w", err))
-	}
-	if _, err = c.kube.CoreV1().Services(aiGatewayRoute.Namespace).Create(ctx, service, metav1.CreateOptions{}); client.IgnoreAlreadyExists(err) != nil {
-		return fmt.Errorf("failed to create Service %s.%s: %w", name, aiGatewayRoute.Namespace, err)
-	}
-	return nil
-}
-
-// mountBackendSecurityPolicySecrets will mount secrets based on backendSecurityPolicies attached to AIServiceBackend.
-func (c *configSink) mountBackendSecurityPolicySecrets(ctx context.Context, spec *corev1.PodSpec, aiGatewayRoute *aigv1a1.AIGatewayRoute) (*corev1.PodSpec, error) {
-	// Mount from scratch to avoid secrets that should be unmounted.
-	// Only keep the original mount which should be the config volume.
-	spec.Volumes = spec.Volumes[:1]
-	container := &spec.Containers[0]
-	container.VolumeMounts = container.VolumeMounts[:1]
-
-	for i := range aiGatewayRoute.Spec.Rules {
-		rule := &aiGatewayRoute.Spec.Rules[i]
-		for j := range rule.BackendRefs {
-			backendRef := &rule.BackendRefs[j]
-			backend, err := c.backend(ctx, aiGatewayRoute.Namespace, backendRef.Name)
-			if err != nil {
-				return nil, fmt.Errorf("failed to get backend %s: %w", backendRef.Name, err)
-			}
-
-			if backendSecurityPolicyRef := backend.Spec.BackendSecurityPolicyRef; backendSecurityPolicyRef != nil {
-				backendSecurityPolicy, err := c.backendSecurityPolicy(ctx, aiGatewayRoute.Namespace, string(backendSecurityPolicyRef.Name))
-				if err != nil {
-					return nil, fmt.Errorf("failed to get backend security policy %s: %w", backendSecurityPolicyRef.Name, err)
-				}
-
-				var secretName string
-				switch backendSecurityPolicy.Spec.Type {
-				case aigv1a1.BackendSecurityPolicyTypeAPIKey:
-					secretName = string(backendSecurityPolicy.Spec.APIKey.SecretRef.Name)
-				case aigv1a1.BackendSecurityPolicyTypeAWSCredentials:
-					if backendSecurityPolicy.Spec.AWSCredentials.CredentialsFile != nil {
-						secretName = string(backendSecurityPolicy.Spec.AWSCredentials.CredentialsFile.SecretRef.Name)
-					} else {
-						secretName = rotators.GetBSPSecretName(backendSecurityPolicy.Name)
-					}
-				default:
-					return nil, fmt.Errorf("backend security policy %s is not supported", backendSecurityPolicy.Spec.Type)
-				}
-
-				volumeName := backendSecurityPolicyVolumeName(i, j, string(backend.Spec.BackendSecurityPolicyRef.Name))
-				spec.Volumes = append(spec.Volumes, corev1.Volume{
-					Name: volumeName,
-					VolumeSource: corev1.VolumeSource{
-						Secret: &corev1.SecretVolumeSource{SecretName: secretName},
-					},
-				})
-
-				container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
-					Name:      volumeName,
-					MountPath: backendSecurityMountPath(volumeName),
-					ReadOnly:  true,
-				})
-			}
-		}
-	}
-	return spec, nil
-}
-
-// syncSecret syncs the state of all resource referencing the given secret.
-func (c *configSink) syncSecret(ctx context.Context, namespace, name string) {
-	var backendSecurityPolicies aigv1a1.BackendSecurityPolicyList
-	err := c.client.List(ctx, &backendSecurityPolicies,
-		client.MatchingFields{
-			k8sClientIndexSecretToReferencingBackendSecurityPolicy: backendSecurityPolicyKey(namespace, name),
-		},
-	)
-	if err != nil {
-		c.logger.Error(err, "failed to list BackendSecurityPolicy", "secret", name, "namespace", namespace)
-		return
-	}
-	for i := range backendSecurityPolicies.Items {
-		backendSecurityPolicy := &backendSecurityPolicies.Items[i]
-		c.syncBackendSecurityPolicy(ctx, backendSecurityPolicy)
-	}
-}
-
-func backendSecurityPolicyVolumeName(ruleIndex, backendRefIndex int, name string) string {
-	// Note: do not use "." as it's not allowed in the volume name.
-	return fmt.Sprintf("rule%d-backref%d-%s", ruleIndex, backendRefIndex, name)
-}
-
-func backendSecurityMountPath(backendSecurityPolicyKey string) string {
-	return fmt.Sprintf("%s/%s", mountedExtProcSecretPath, backendSecurityPolicyKey)
-}


Test patch:
diff --git a/internal/controller/ai_gateway_route_test.go b/internal/controller/ai_gateway_route_test.go
index 37c11f7..c778cae 100644
--- a/internal/controller/ai_gateway_route_test.go
+++ b/internal/controller/ai_gateway_route_test.go
@@ -6,62 +6,70 @@
 package controller
 
 import (
+	"context"
+	"fmt"
+	"strconv"
 	"testing"
+	"time"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/go-logr/logr"
 	"github.com/stretchr/testify/require"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
+	uuid2 "k8s.io/apimachinery/pkg/util/uuid"
+	"k8s.io/apimachinery/pkg/util/yaml"
 	fake2 "k8s.io/client-go/kubernetes/fake"
 	"k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 
 	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
 	"github.com/envoyproxy/ai-gateway/filterapi"
+	"github.com/envoyproxy/ai-gateway/internal/controller/rotators"
 )
 
 func TestAIGatewayRouteController_Reconcile(t *testing.T) {
-	ch := make(chan ConfigSinkEvent, 100)
-	cl := fake.NewClientBuilder().WithScheme(scheme).Build()
-	c := NewAIGatewayRouteController(cl, fake2.NewClientset(), ctrl.Log, ch)
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	c := NewAIGatewayRouteController(fakeClient, fake2.NewClientset(), ctrl.Log, "gcr.io/ai-gateway/extproc:latest", "info")
 
-	err := cl.Create(t.Context(), &aigv1a1.AIGatewayRoute{ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "default"}})
+	err := fakeClient.Create(t.Context(), &aigv1a1.AIGatewayRoute{ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "default"}})
 	require.NoError(t, err)
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "myroute"}})
 	require.NoError(t, err)
-	item, ok := <-ch
-	require.True(t, ok)
-	require.IsType(t, &aigv1a1.AIGatewayRoute{}, item)
-	require.Equal(t, "myroute", item.(*aigv1a1.AIGatewayRoute).Name)
-	require.Equal(t, "default", item.(*aigv1a1.AIGatewayRoute).Namespace)
 
 	// Do it for the second time with a slightly different configuration.
-	current := item.(*aigv1a1.AIGatewayRoute)
+	var current aigv1a1.AIGatewayRoute
+	err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "myroute"}, &current)
+	require.NoError(t, err)
+	current.Spec.APISchema = aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"}
 	current.Spec.TargetRefs = []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
 		{LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{Name: "mytarget"}},
 	}
-	err = cl.Update(t.Context(), current)
+	err = fakeClient.Update(t.Context(), &current)
 	require.NoError(t, err)
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "myroute"}})
 	require.NoError(t, err)
-	item, ok = <-ch
-	require.True(t, ok)
-	require.IsType(t, &aigv1a1.AIGatewayRoute{}, item)
-	r := item.(*aigv1a1.AIGatewayRoute)
-	require.Equal(t, "myroute", r.Name)
-	require.Equal(t, "default", r.Namespace)
-	require.Len(t, r.Spec.TargetRefs, 1)
-	require.Equal(t, "mytarget", string(r.Spec.TargetRefs[0].Name))
+
+	var updated aigv1a1.AIGatewayRoute
+	err = fakeClient.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "myroute"}, &updated)
+	require.NoError(t, err)
+
+	require.Equal(t, "myroute", updated.Name)
+	require.Equal(t, "default", updated.Namespace)
+	require.Len(t, updated.Spec.TargetRefs, 1)
+	require.Equal(t, "mytarget", string(updated.Spec.TargetRefs[0].Name))
+	require.Equal(t, aigv1a1.APISchemaOpenAI, updated.Spec.APISchema.Name)
 
 	// Test the case where the AIGatewayRoute is being deleted.
-	err = cl.Delete(t.Context(), &aigv1a1.AIGatewayRoute{ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "default"}})
+	err = fakeClient.Delete(t.Context(), &aigv1a1.AIGatewayRoute{ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "default"}})
 	require.NoError(t, err)
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "myroute"}})
 	require.NoError(t, err)
@@ -73,7 +81,7 @@ func Test_extProcName(t *testing.T) {
 }
 
 func TestAIGatewayRouteController_ensuresExtProcConfigMapExists(t *testing.T) {
-	c := &aiGatewayRouteController{client: fake.NewClientBuilder().WithScheme(scheme).Build()}
+	c := &AIGatewayRouteController{client: fake.NewClientBuilder().WithScheme(scheme).Build()}
 	c.kube = fake2.NewClientset()
 	name := "myroute"
 	ownerRef := []metav1.OwnerReference{
@@ -97,7 +105,7 @@ func TestAIGatewayRouteController_ensuresExtProcConfigMapExists(t *testing.T) {
 }
 
 func TestAIGatewayRouteController_reconcileExtProcExtensionPolicy(t *testing.T) {
-	c := &aiGatewayRouteController{client: fake.NewClientBuilder().WithScheme(scheme).Build()}
+	c := &AIGatewayRouteController{client: fake.NewClientBuilder().WithScheme(scheme).Build()}
 	name := "myroute"
 	ownerRef := []metav1.OwnerReference{
 		{APIVersion: "aigateway.envoyproxy.io/v1alpha1", Kind: "AIGatewayRoute", Name: name, Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true)},
@@ -186,3 +194,833 @@ func Test_applyExtProcDeploymentConfigUpdate(t *testing.T) {
 		require.Equal(t, int32(123), *dep.Replicas)
 	})
 }
+
+func requireNewFakeClientWithIndexes(t *testing.T) client.Client {
+	builder := fake.NewClientBuilder().WithScheme(scheme)
+	err := ApplyIndexing(t.Context(), func(_ context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error {
+		builder = builder.WithIndex(obj, field, extractValue)
+		return nil
+	})
+	require.NoError(t, err)
+	return builder.Build()
+}
+
+func TestAIGatewayRouterController_syncAIGatewayRoute(t *testing.T) {
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	kube := fake2.NewClientset()
+
+	s := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), "defaultExtProcImage", "debug")
+	require.NotNil(t, s)
+
+	for _, backend := range []*aigv1a1.AIServiceBackend{
+		{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns1"}, Spec: aigv1a1.AIServiceBackendSpec{
+			BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
+		}},
+		{ObjectMeta: metav1.ObjectMeta{Name: "orange", Namespace: "ns1"}, Spec: aigv1a1.AIServiceBackendSpec{
+			BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
+		}},
+	} {
+		err := fakeClient.Create(t.Context(), backend, &client.CreateOptions{})
+		require.NoError(t, err)
+	}
+
+	t.Run("existing", func(t *testing.T) {
+		route := &aigv1a1.AIGatewayRoute{
+			ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
+			Spec: aigv1a1.AIGatewayRouteSpec{
+				Rules: []aigv1a1.AIGatewayRouteRule{
+					{
+						BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "apple", Weight: 1}, {Name: "orange", Weight: 1}},
+					},
+				},
+				APISchema: aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"},
+			},
+		}
+		err := fakeClient.Create(t.Context(), route, &client.CreateOptions{})
+		require.NoError(t, err)
+		httpRoute := &gwapiv1.HTTPRoute{
+			ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1", Labels: map[string]string{managedByLabel: "envoy-ai-gateway"}},
+			Spec:       gwapiv1.HTTPRouteSpec{},
+		}
+		err = fakeClient.Create(t.Context(), httpRoute, &client.CreateOptions{})
+		require.NoError(t, err)
+
+		// Create the initial configmap.
+		_, err = kube.CoreV1().ConfigMaps(route.Namespace).Create(t.Context(), &corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{Name: extProcName(route), Namespace: route.Namespace},
+		}, metav1.CreateOptions{})
+		require.NoError(t, err)
+
+		// Then sync, which should update the HTTPRoute.
+		require.NoError(t, s.syncAIGatewayRoute(t.Context(), route))
+		var updatedHTTPRoute gwapiv1.HTTPRoute
+		err = fakeClient.Get(t.Context(), client.ObjectKey{Name: "route1", Namespace: "ns1"}, &updatedHTTPRoute)
+		require.NoError(t, err)
+		require.Len(t, updatedHTTPRoute.Spec.Rules, 3) // 2 backends + 1 for the default rule.
+		require.Len(t, updatedHTTPRoute.Spec.Rules[0].BackendRefs, 1)
+		require.Equal(t, "some-backend1", string(updatedHTTPRoute.Spec.Rules[0].BackendRefs[0].BackendRef.Name))
+		require.Equal(t, "apple.ns1", updatedHTTPRoute.Spec.Rules[0].Matches[0].Headers[0].Value)
+		require.Equal(t, "some-backend2", string(updatedHTTPRoute.Spec.Rules[1].BackendRefs[0].BackendRef.Name))
+		require.Equal(t, "orange.ns1", updatedHTTPRoute.Spec.Rules[1].Matches[0].Headers[0].Value)
+		// Defaulting to the first backend.
+		require.Equal(t, "some-backend1", string(updatedHTTPRoute.Spec.Rules[2].BackendRefs[0].BackendRef.Name))
+		require.Equal(t, "/", *updatedHTTPRoute.Spec.Rules[2].Matches[0].Path.Value)
+	})
+
+	// Check the namespace has the default host rewrite filter.
+	var f egv1a1.HTTPRouteFilter
+	err := s.client.Get(t.Context(), client.ObjectKey{Name: hostRewriteHTTPFilterName, Namespace: "ns1"}, &f)
+	require.NoError(t, err)
+	require.Equal(t, hostRewriteHTTPFilterName, f.Name)
+}
+
+func Test_newHTTPRoute(t *testing.T) {
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	s := NewAIGatewayRouteController(fakeClient, nil, logr.Discard(), "defaultExtProcImage", "debug")
+	httpRoute := &gwapiv1.HTTPRoute{
+		ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
+		Spec:       gwapiv1.HTTPRouteSpec{},
+	}
+	aiGatewayRoute := &aigv1a1.AIGatewayRoute{
+		ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
+		Spec: aigv1a1.AIGatewayRouteSpec{
+			TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+				{
+					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+						Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
+					},
+				},
+			},
+			Rules: []aigv1a1.AIGatewayRouteRule{
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "apple", Weight: 100}},
+				},
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+						{Name: "orange", Weight: 100},
+						{Name: "apple", Weight: 100},
+						{Name: "pineapple", Weight: 100},
+					},
+				},
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "foo", Weight: 1}},
+				},
+			},
+		},
+	}
+	for _, backend := range []*aigv1a1.AIServiceBackend{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns1"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "orange", Namespace: "ns1"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns1"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns1"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
+			},
+		},
+	} {
+		err := s.client.Create(t.Context(), backend, &client.CreateOptions{})
+		require.NoError(t, err)
+	}
+	err := s.newHTTPRoute(t.Context(), httpRoute, aiGatewayRoute)
+	require.NoError(t, err)
+
+	expRules := []gwapiv1.HTTPRouteRule{
+		{
+			Matches: []gwapiv1.HTTPRouteMatch{
+				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "apple.ns1"}}},
+			},
+			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
+		},
+		{
+			Matches: []gwapiv1.HTTPRouteMatch{
+				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "orange.ns1"}}},
+			},
+			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
+		},
+		{
+			Matches: []gwapiv1.HTTPRouteMatch{
+				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "pineapple.ns1"}}},
+			},
+			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
+		},
+		{
+			Matches: []gwapiv1.HTTPRouteMatch{
+				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "foo.ns1"}}},
+			},
+			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
+		},
+	}
+	require.Len(t, httpRoute.Spec.Rules, 5) // 4 backends + 1 for the default rule.
+	for i, r := range httpRoute.Spec.Rules {
+		t.Run(fmt.Sprintf("rule-%d", i), func(t *testing.T) {
+			if i == 4 {
+				require.Equal(t, expRules[0].BackendRefs, r.BackendRefs)
+				require.NotNil(t, r.Matches[0].Path)
+				require.Equal(t, "/", *r.Matches[0].Path.Value)
+			} else {
+				require.Equal(t, expRules[i].Matches, r.Matches)
+				require.Equal(t, expRules[i].BackendRefs, r.BackendRefs)
+			}
+
+			// Each rule should have a host rewrite filter by default.
+			require.Len(t, r.Filters, 1)
+			require.Equal(t, gwapiv1.HTTPRouteFilterExtensionRef, r.Filters[0].Type)
+			require.NotNil(t, r.Filters[0].ExtensionRef)
+			require.Equal(t, hostRewriteHTTPFilterName, string(r.Filters[0].ExtensionRef.Name))
+		})
+	}
+}
+
+func TestAIGatewayRouteController_updateExtProcConfigMap(t *testing.T) {
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	kube := fake2.NewClientset()
+
+	s := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), "defaultExtProcImage", "debug")
+	require.NoError(t, fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy"}}))
+	require.NoError(t, fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-2"}}))
+
+	for _, bsp := range []*aigv1a1.BackendSecurityPolicy{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-1", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
+				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
+					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-2", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
+				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
+					Region: "us-east-1",
+					CredentialsFile: &aigv1a1.AWSCredentialsFile{
+						SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+						Profile:   "default",
+					},
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-3", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
+				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
+					Region:            "us-east-1",
+					OIDCExchangeToken: &aigv1a1.AWSOIDCExchangeToken{},
+				},
+			},
+		},
+	} {
+		err := fakeClient.Create(t.Context(), bsp, &client.CreateOptions{})
+		require.NoError(t, err)
+	}
+
+	for _, b := range []*aigv1a1.AIServiceBackend{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				APISchema: aigv1a1.VersionedAPISchema{
+					Name: aigv1a1.APISchemaAWSBedrock,
+				},
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "cat", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "pen", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-2"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "dog", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend5", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-3"},
+			},
+		},
+	} {
+		err := fakeClient.Create(t.Context(), b, &client.CreateOptions{})
+		require.NoError(t, err)
+	}
+	require.NotNil(t, s)
+
+	for _, tc := range []struct {
+		name  string
+		route *aigv1a1.AIGatewayRoute
+		exp   *filterapi.Config
+	}{
+		{
+			name: "basic",
+			route: &aigv1a1.AIGatewayRoute{
+				ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"},
+				Spec: aigv1a1.AIGatewayRouteSpec{
+					APISchema: aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"},
+					Rules: []aigv1a1.AIGatewayRouteRule{
+						{
+							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+								{Name: "apple", Weight: 1},
+								{Name: "pineapple", Weight: 2},
+							},
+							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}}},
+							},
+						},
+						{
+							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "cat", Weight: 1}},
+							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai"}}},
+							},
+						},
+						{
+							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+								{Name: "pen", Weight: 2},
+							},
+							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-2"}}},
+							},
+						},
+						{
+							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+								{Name: "dog", Weight: 1},
+							},
+							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-3"}}},
+							},
+						},
+					},
+					LLMRequestCosts: []aigv1a1.LLMRequestCost{
+						{
+							Type:        aigv1a1.LLMRequestCostTypeOutputToken,
+							MetadataKey: "output-token",
+						},
+						{
+							Type:        aigv1a1.LLMRequestCostTypeInputToken,
+							MetadataKey: "input-token",
+						},
+						{
+							Type:        aigv1a1.LLMRequestCostTypeTotalToken,
+							MetadataKey: "total-token",
+						},
+						{
+							Type:          aigv1a1.LLMRequestCostTypeCEL,
+							MetadataKey:   "cel-token",
+							CELExpression: ptr.To("model == 'cool_model' ?  input_tokens * output_tokens : total_tokens"),
+						},
+					},
+				},
+			},
+			exp: &filterapi.Config{
+				UUID:                     string(uuid2.NewUUID()),
+				Schema:                   filterapi.VersionedAPISchema{Name: filterapi.APISchemaOpenAI, Version: "v123"},
+				ModelNameHeaderKey:       aigv1a1.AIModelHeaderKey,
+				MetadataNamespace:        aigv1a1.AIGatewayFilterMetadataNamespace,
+				SelectedBackendHeaderKey: selectedBackendHeaderKey,
+				Rules: []filterapi.RouteRule{
+					{
+						Backends: []filterapi.Backend{
+							{Name: "apple.ns", Weight: 1, Schema: filterapi.VersionedAPISchema{Name: filterapi.APISchemaAWSBedrock}, Auth: &filterapi.BackendAuth{
+								APIKey: &filterapi.APIKeyAuth{
+									Filename: "/etc/backend_security_policy/rule0-backref0-some-backend-security-policy-1/apiKey",
+								},
+							}}, {Name: "pineapple.ns", Weight: 2},
+						},
+						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}},
+					},
+					{
+						Backends: []filterapi.Backend{{Name: "cat.ns", Weight: 1, Auth: &filterapi.BackendAuth{
+							APIKey: &filterapi.APIKeyAuth{
+								Filename: "/etc/backend_security_policy/rule1-backref0-some-backend-security-policy-1/apiKey",
+							},
+						}}},
+						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai"}},
+					},
+					{
+						Backends: []filterapi.Backend{{Name: "pen.ns", Weight: 2, Auth: &filterapi.BackendAuth{
+							AWSAuth: &filterapi.AWSAuth{
+								CredentialFileName: "/etc/backend_security_policy/rule2-backref0-some-backend-security-policy-2/credentials",
+								Region:             "us-east-1",
+							},
+						}}},
+						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-2"}},
+					},
+					{
+						Backends: []filterapi.Backend{{Name: "dog.ns", Weight: 1, Auth: &filterapi.BackendAuth{
+							AWSAuth: &filterapi.AWSAuth{
+								CredentialFileName: "/etc/backend_security_policy/rule3-backref0-some-backend-security-policy-3/credentials",
+								Region:             "us-east-1",
+							},
+						}}},
+						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-3"}},
+					},
+				},
+				LLMRequestCosts: []filterapi.LLMRequestCost{
+					{Type: filterapi.LLMRequestCostTypeOutputToken, MetadataKey: "output-token"},
+					{Type: filterapi.LLMRequestCostTypeInputToken, MetadataKey: "input-token"},
+					{Type: filterapi.LLMRequestCostTypeTotalToken, MetadataKey: "total-token"},
+					{Type: filterapi.LLMRequestCostTypeCELExpression, MetadataKey: "cel-token", CELExpression: "model == 'cool_model' ?  input_tokens * output_tokens : total_tokens"},
+				},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := s.kube.CoreV1().ConfigMaps(tc.route.Namespace).Create(t.Context(), &corev1.ConfigMap{
+				ObjectMeta: metav1.ObjectMeta{Name: extProcName(tc.route), Namespace: tc.route.Namespace},
+			}, metav1.CreateOptions{})
+			require.NoError(t, err)
+
+			err = s.updateExtProcConfigMap(t.Context(), tc.route, tc.exp.UUID)
+			require.NoError(t, err)
+
+			cm, err := s.kube.CoreV1().ConfigMaps(tc.route.Namespace).Get(t.Context(), extProcName(tc.route), metav1.GetOptions{})
+			require.NoError(t, err)
+			require.NotNil(t, cm)
+
+			data := cm.Data[expProcConfigFileName]
+			var actual filterapi.Config
+			require.NoError(t, yaml.Unmarshal([]byte(data), &actual))
+			require.Equal(t, tc.exp, &actual)
+		})
+	}
+}
+
+func TestAIGatewayRouteController_syncExtProcDeployment(t *testing.T) {
+	t.Skip()
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	kube := fake2.NewClientset()
+
+	s := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), "envoyproxy/ai-gateway-extproc:foo", "debug")
+	err := fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy"}})
+	require.NoError(t, err)
+
+	for _, bsp := range []*aigv1a1.BackendSecurityPolicy{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-1", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
+				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
+					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				},
+			},
+		},
+	} {
+		require.NoError(t, fakeClient.Create(t.Context(), bsp, &client.CreateOptions{}))
+	}
+
+	for _, b := range []*aigv1a1.AIServiceBackend{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				APISchema: aigv1a1.VersionedAPISchema{
+					Name: aigv1a1.APISchemaAWSBedrock,
+				},
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "cat", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+			},
+		},
+	} {
+		require.NoError(t, fakeClient.Create(t.Context(), b, &client.CreateOptions{}))
+	}
+	require.NotNil(t, s)
+
+	aiGatewayRoute := &aigv1a1.AIGatewayRoute{
+		ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"},
+		TypeMeta: metav1.TypeMeta{
+			Kind: "AIGatewayRoute", // aiGatewayRoute controller typically adds these type meta
+		},
+		Spec: aigv1a1.AIGatewayRouteSpec{
+			FilterConfig: &aigv1a1.AIGatewayFilterConfig{
+				Type: aigv1a1.AIGatewayFilterConfigTypeExternalProcess,
+				ExternalProcess: &aigv1a1.AIGatewayFilterConfigExternalProcess{
+					Replicas: ptr.To[int32](123),
+					Resources: &corev1.ResourceRequirements{
+						Limits: corev1.ResourceList{
+							corev1.ResourceCPU:    resource.MustParse("200m"),
+							corev1.ResourceMemory: resource.MustParse("100Mi"),
+						},
+					},
+				},
+			},
+			APISchema: aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"},
+			Rules: []aigv1a1.AIGatewayRouteRule{
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+						{Name: "apple", Weight: 1},
+						{Name: "pineapple", Weight: 2},
+					},
+					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}}},
+					},
+				},
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "cat", Weight: 1}},
+					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai"}}},
+					},
+				},
+			},
+			TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+				{
+					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+						Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
+					},
+				},
+			},
+		},
+	}
+
+	require.NoError(t, fakeClient.Create(t.Context(), aiGatewayRoute, &client.CreateOptions{}))
+
+	t.Run("create", func(t *testing.T) {
+		err = s.syncExtProcDeployment(t.Context(), aiGatewayRoute)
+		require.NoError(t, err)
+
+		resourceLimits := &corev1.ResourceRequirements{
+			Limits: corev1.ResourceList{
+				corev1.ResourceCPU:    resource.MustParse("200m"),
+				corev1.ResourceMemory: resource.MustParse("100Mi"),
+			},
+		}
+		require.Eventually(t, func() bool {
+			extProcDeployment, err := s.kube.AppsV1().Deployments("ns").Get(t.Context(), extProcName(aiGatewayRoute), metav1.GetOptions{})
+			if err != nil {
+				t.Logf("failed to get deployment %s: %v", extProcName(aiGatewayRoute), err)
+				return false
+			}
+			require.Equal(t, "envoyproxy/ai-gateway-extproc:foo", extProcDeployment.Spec.Template.Spec.Containers[0].Image)
+			require.Len(t, extProcDeployment.OwnerReferences, 1)
+			require.Equal(t, "myroute", extProcDeployment.OwnerReferences[0].Name)
+			require.Equal(t, "AIGatewayRoute", extProcDeployment.OwnerReferences[0].Kind)
+			require.Equal(t, int32(123), *extProcDeployment.Spec.Replicas)
+			require.Equal(t, resourceLimits, &extProcDeployment.Spec.Template.Spec.Containers[0].Resources)
+			return true
+		}, 30*time.Second, 200*time.Millisecond)
+
+		service, err := s.kube.CoreV1().Services("ns").Get(t.Context(), extProcName(aiGatewayRoute), metav1.GetOptions{})
+		require.NoError(t, err)
+		require.Equal(t, extProcName(aiGatewayRoute), service.Name)
+	})
+
+	t.Run("update", func(t *testing.T) {
+		// Update fields in resource again
+		// Doing it again should not fail and update the deployment.
+		newResourceLimits := &corev1.ResourceRequirements{
+			Limits: corev1.ResourceList{
+				corev1.ResourceCPU:    resource.MustParse("300m"),
+				corev1.ResourceMemory: resource.MustParse("32Mi"),
+			},
+		}
+		aiGatewayRoute.Spec.FilterConfig.ExternalProcess.Resources = newResourceLimits
+		aiGatewayRoute.Spec.FilterConfig.ExternalProcess.Replicas = ptr.To[int32](456)
+
+		require.NoError(t, s.syncExtProcDeployment(t.Context(), aiGatewayRoute))
+		// Check the deployment is updated.
+		require.Eventually(t, func() bool {
+			extProcDeployment, err := s.kube.AppsV1().Deployments("ns").Get(t.Context(), extProcName(aiGatewayRoute), metav1.GetOptions{})
+			if err != nil {
+				t.Logf("failed to get deployment %s: %v", extProcName(aiGatewayRoute), err)
+				return false
+			}
+			require.Equal(t, "envoyproxy/ai-gateway-extproc:foo", extProcDeployment.Spec.Template.Spec.Containers[0].Image)
+			require.Len(t, extProcDeployment.OwnerReferences, 1)
+			require.Equal(t, "myroute", extProcDeployment.OwnerReferences[0].Name)
+			require.Equal(t, "AIGatewayRoute", extProcDeployment.OwnerReferences[0].Kind)
+			require.Equal(t, int32(456), *extProcDeployment.Spec.Replicas)
+			require.Equal(t, newResourceLimits, &extProcDeployment.Spec.Template.Spec.Containers[0].Resources)
+
+			for _, v := range extProcDeployment.Spec.Template.Spec.Containers[0].VolumeMounts {
+				require.True(t, v.ReadOnly)
+			}
+			return true
+		}, 30*time.Second, 200*time.Millisecond)
+	})
+}
+
+func TestAIGatewayRouteController_MountBackendSecurityPolicySecrets(t *testing.T) {
+	// Create simple case
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	kube := fake2.NewClientset()
+
+	c := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), "defaultExtProcImage", "debug")
+	require.NoError(t, fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy"}}))
+
+	for _, secret := range []*corev1.Secret{
+		{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-1"}},
+		{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-2"}},
+		{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-3"}},
+		{ObjectMeta: metav1.ObjectMeta{Name: "aws-oidc-name"}},
+	} {
+		require.NoError(t, fakeClient.Create(t.Context(), secret, &client.CreateOptions{}))
+	}
+
+	for _, bsp := range []*aigv1a1.BackendSecurityPolicy{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-other-backend-security-policy-1", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
+				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
+					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-other-backend-security-policy-2", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
+				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
+					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "aws-oidc-name", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
+				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
+					OIDCExchangeToken: &aigv1a1.AWSOIDCExchangeToken{},
+					Region:            "us-east-1",
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "some-other-backend-security-policy-aws", Namespace: "ns"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
+				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
+					CredentialsFile: &aigv1a1.AWSCredentialsFile{
+						SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+						Profile:   "default",
+					},
+					Region: "us-east-1",
+				},
+			},
+		},
+	} {
+		require.NoError(t, fakeClient.Create(t.Context(), bsp, &client.CreateOptions{}))
+	}
+
+	for _, backend := range []*aigv1a1.AIServiceBackend{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				APISchema: aigv1a1.VersionedAPISchema{
+					Name: aigv1a1.APISchemaAWSBedrock,
+				},
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-other-backend-security-policy-1"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				APISchema: aigv1a1.VersionedAPISchema{
+					Name: aigv1a1.APISchemaAWSBedrock,
+				},
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-other-backend-security-policy-aws"},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "dog", Namespace: "ns"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				APISchema: aigv1a1.VersionedAPISchema{
+					Name: aigv1a1.APISchemaAWSBedrock,
+				},
+				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "aws-oidc-name"},
+			},
+		},
+	} {
+		require.NoError(t, fakeClient.Create(t.Context(), backend, &client.CreateOptions{}))
+		require.NotNil(t, c)
+	}
+
+	aiGateway := aigv1a1.AIGatewayRoute{
+		ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"},
+		Spec: aigv1a1.AIGatewayRouteSpec{
+			Rules: []aigv1a1.AIGatewayRouteRule{
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+						{Name: "apple", Weight: 1},
+					},
+					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}}},
+					},
+				},
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+						{Name: "pineapple", Weight: 1},
+					},
+					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai-2"}}},
+					},
+				},
+				{
+					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
+						{Name: "dog", Weight: 1},
+					},
+					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
+						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai-3"}}},
+					},
+				},
+			},
+		},
+	}
+
+	spec := corev1.PodSpec{
+		Volumes: []corev1.Volume{
+			{
+				Name: "extproc-config",
+				VolumeSource: corev1.VolumeSource{
+					ConfigMap: &corev1.ConfigMapVolumeSource{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "extproc-config",
+						},
+					},
+				},
+			},
+		},
+		Containers: []corev1.Container{
+			{VolumeMounts: []corev1.VolumeMount{{Name: "extproc-config", MountPath: "some-path", ReadOnly: true}}},
+		},
+	}
+
+	require.NoError(t, fakeClient.Create(t.Context(), &aiGateway, &client.CreateOptions{}))
+
+	updatedSpec, err := c.mountBackendSecurityPolicySecrets(t.Context(), &spec, &aiGateway)
+	require.NoError(t, err)
+
+	require.Len(t, updatedSpec.Volumes, 4)
+	require.Len(t, updatedSpec.Containers[0].VolumeMounts, 4)
+	// API Key.
+	require.Equal(t, "some-secret-policy-1", updatedSpec.Volumes[1].VolumeSource.Secret.SecretName)
+	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-1", updatedSpec.Volumes[1].Name)
+	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-1", updatedSpec.Containers[0].VolumeMounts[1].Name)
+	require.Equal(t, "/etc/backend_security_policy/rule0-backref0-some-other-backend-security-policy-1", updatedSpec.Containers[0].VolumeMounts[1].MountPath)
+	// AWS CredentialFile.
+	require.Equal(t, "some-secret-policy-3", updatedSpec.Volumes[2].VolumeSource.Secret.SecretName)
+	require.Equal(t, "rule1-backref0-some-other-backend-security-policy-aws", updatedSpec.Volumes[2].Name)
+	require.Equal(t, "rule1-backref0-some-other-backend-security-policy-aws", updatedSpec.Containers[0].VolumeMounts[2].Name)
+	require.Equal(t, "/etc/backend_security_policy/rule1-backref0-some-other-backend-security-policy-aws", updatedSpec.Containers[0].VolumeMounts[2].MountPath)
+	// AWS OIDC.
+	require.Equal(t, rotators.GetBSPSecretName("aws-oidc-name"), updatedSpec.Volumes[3].VolumeSource.Secret.SecretName)
+	require.Equal(t, "rule2-backref0-aws-oidc-name", updatedSpec.Volumes[3].Name)
+	require.Equal(t, "rule2-backref0-aws-oidc-name", updatedSpec.Containers[0].VolumeMounts[3].Name)
+	require.Equal(t, "/etc/backend_security_policy/rule2-backref0-aws-oidc-name", updatedSpec.Containers[0].VolumeMounts[3].MountPath)
+
+	require.NoError(t, fakeClient.Delete(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"}}, &client.DeleteOptions{}))
+
+	// Update to new security policy.
+	backend := aigv1a1.AIServiceBackend{
+		ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
+		Spec: aigv1a1.AIServiceBackendSpec{
+			APISchema: aigv1a1.VersionedAPISchema{
+				Name: aigv1a1.APISchemaAWSBedrock,
+			},
+			BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
+			BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-other-backend-security-policy-2"},
+		},
+	}
+
+	require.NoError(t, fakeClient.Create(t.Context(), &backend, &client.CreateOptions{}))
+	require.NotNil(t, c)
+
+	updatedSpec, err = c.mountBackendSecurityPolicySecrets(t.Context(), &spec, &aiGateway)
+	require.NoError(t, err)
+
+	require.Len(t, updatedSpec.Volumes, 4)
+	require.Len(t, updatedSpec.Containers[0].VolumeMounts, 4)
+	require.Equal(t, "some-secret-policy-2", updatedSpec.Volumes[1].VolumeSource.Secret.SecretName)
+	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-2", updatedSpec.Volumes[1].Name)
+	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-2", updatedSpec.Containers[0].VolumeMounts[1].Name)
+	require.Equal(t, "/etc/backend_security_policy/rule0-backref0-some-other-backend-security-policy-2", updatedSpec.Containers[0].VolumeMounts[1].MountPath)
+
+	for _, v := range updatedSpec.Containers[0].VolumeMounts {
+		require.True(t, v.ReadOnly, v.Name)
+	}
+}
+
+func Test_backendSecurityPolicyVolumeName(t *testing.T) {
+	mountPath := backendSecurityPolicyVolumeName(1, 2, "name")
+	require.Equal(t, "rule1-backref2-name", mountPath)
+}
+
+func TestAIGatewayRouteController_AnnotateExtProcPods(t *testing.T) {
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	kube := fake2.NewClientset()
+
+	s := NewAIGatewayRouteController(fakeClient, kube, logr.Discard(), "defaultExtProcImage", "debug")
+
+	aiGatewayRoute := &aigv1a1.AIGatewayRoute{
+		ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "foons"},
+	}
+
+	for i := range 5 {
+		pod := &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "somepod" + strconv.Itoa(i),
+				Namespace: "foons",
+				Labels:    map[string]string{"app": extProcName(aiGatewayRoute)},
+			},
+			Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "someapp"}}},
+		}
+		_, err := kube.CoreV1().Pods("foons").Create(t.Context(), pod, metav1.CreateOptions{})
+		require.NoError(t, err)
+	}
+
+	uuid := string(uuid2.NewUUID())
+	err := s.annotateExtProcPods(t.Context(), aiGatewayRoute, uuid)
+	require.NoError(t, err)
+
+	// Check that all pods have been annotated.
+	for i := range 5 {
+		pod, err := kube.CoreV1().Pods("foons").Get(t.Context(), "somepod"+strconv.Itoa(i), metav1.GetOptions{})
+		require.NoError(t, err)
+		require.Equal(t, uuid, pod.Annotations[extProcConfigAnnotationKey])
+	}
+}
diff --git a/internal/controller/ai_service_backend_test.go b/internal/controller/ai_service_backend_test.go
index 9da9dca..67c6c7b 100644
--- a/internal/controller/ai_service_backend_test.go
+++ b/internal/controller/ai_service_backend_test.go
@@ -18,27 +18,66 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 
 	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
+	internaltesting "github.com/envoyproxy/ai-gateway/internal/testing"
 )
 
 func TestAIServiceBackendController_Reconcile(t *testing.T) {
-	ch := make(chan ConfigSinkEvent, 100)
-	cl := fake.NewClientBuilder().WithScheme(scheme).Build()
-	c := NewAIServiceBackendController(cl, fake2.NewClientset(), ctrl.Log, ch)
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	syncFn := internaltesting.NewSyncFnImpl[aigv1a1.AIGatewayRoute]()
+	c := NewAIServiceBackendController(fakeClient, fake2.NewClientset(), ctrl.Log, syncFn.Sync)
+	originals := []*aigv1a1.AIGatewayRoute{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "default"},
+			Spec: aigv1a1.AIGatewayRouteSpec{
+				TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+					{
+						LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+							Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
+						},
+					},
+				},
+				Rules: []aigv1a1.AIGatewayRouteRule{
+					{
+						Matches:     []aigv1a1.AIGatewayRouteRuleMatch{{}},
+						BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "mybackend"}},
+					},
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "myroute2", Namespace: "default"},
+			Spec: aigv1a1.AIGatewayRouteSpec{
+				TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+					{
+						LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+							Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
+						},
+					},
+				},
+				Rules: []aigv1a1.AIGatewayRouteRule{
+					{
+						Matches:     []aigv1a1.AIGatewayRouteRuleMatch{{}},
+						BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "mybackend"}},
+					},
+				},
+			},
+		},
+	}
+	for _, route := range originals {
+		require.NoError(t, fakeClient.Create(t.Context(), route))
+	}
 
-	err := cl.Create(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "mybackend", Namespace: "default"}})
+	err := fakeClient.Create(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "mybackend", Namespace: "default"}})
 	require.NoError(t, err)
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "mybackend"}})
 	require.NoError(t, err)
-	item, ok := <-ch
-	require.True(t, ok)
-	require.IsType(t, &aigv1a1.AIServiceBackend{}, item)
-	require.Equal(t, "mybackend", item.(*aigv1a1.AIServiceBackend).Name)
-	require.Equal(t, "default", item.(*aigv1a1.AIServiceBackend).Namespace)
+	require.Equal(t, originals, syncFn.GetItems())
 
 	// Test the case where the AIServiceBackend is being deleted.
-	err = cl.Delete(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "mybackend", Namespace: "default"}})
+	err = fakeClient.Delete(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "mybackend", Namespace: "default"}})
 	require.NoError(t, err)
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "mybackend"}})
 	require.NoError(t, err)
diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go
index d554962..521f520 100644
--- a/internal/controller/backend_security_policy_test.go
+++ b/internal/controller/backend_security_policy_test.go
@@ -25,6 +25,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	fake2 "k8s.io/client-go/kubernetes/fake"
+	"k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -33,40 +34,62 @@ import (
 
 	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
 	"github.com/envoyproxy/ai-gateway/internal/controller/rotators"
+	internaltesting "github.com/envoyproxy/ai-gateway/internal/testing"
 )
 
 func TestBackendSecurityController_Reconcile(t *testing.T) {
-	ch := make(chan ConfigSinkEvent, 100)
-	cl := fake.NewClientBuilder().WithScheme(scheme).Build()
-	c := newBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, ch)
+	syncFn := internaltesting.NewSyncFnImpl[aigv1a1.AIServiceBackend]()
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	c := newBackendSecurityPolicyController(fakeClient, fake2.NewClientset(), ctrl.Log, syncFn.Sync)
 	backendSecurityPolicyName := "mybackendSecurityPolicy"
 	namespace := "default"
 
-	err := cl.Create(t.Context(), &aigv1a1.BackendSecurityPolicy{ObjectMeta: metav1.ObjectMeta{Name: backendSecurityPolicyName, Namespace: namespace}})
+	// Create AIServiceBackend that references the BackendSecurityPolicy.
+	asb := &aigv1a1.AIServiceBackend{
+		ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
+		Spec: aigv1a1.AIServiceBackendSpec{
+			BackendRef: gwapiv1.BackendObjectReference{
+				Name: gwapiv1.ObjectName("mybackend"),
+				Port: ptr.To[gwapiv1.PortNumber](8080),
+			},
+			BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{
+				Name: gwapiv1.ObjectName(backendSecurityPolicyName),
+			},
+		},
+	}
+	require.NoError(t, fakeClient.Create(t.Context(), asb))
+
+	err := fakeClient.Create(t.Context(), &aigv1a1.BackendSecurityPolicy{
+		ObjectMeta: metav1.ObjectMeta{Name: backendSecurityPolicyName, Namespace: namespace},
+		Spec: aigv1a1.BackendSecurityPolicySpec{
+			Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
+			APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
+				SecretRef: &gwapiv1.SecretObjectReference{Name: "mysecret"},
+			},
+		},
+	})
 	require.NoError(t, err)
 	res, err := c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: backendSecurityPolicyName}})
 	require.NoError(t, err)
 	require.False(t, res.Requeue)
-	item, ok := <-ch
-	require.True(t, ok)
-	require.IsType(t, &aigv1a1.BackendSecurityPolicy{}, item)
-	require.Equal(t, backendSecurityPolicyName, item.(*aigv1a1.BackendSecurityPolicy).Name)
-	require.Equal(t, namespace, item.(*aigv1a1.BackendSecurityPolicy).Namespace)
+	items := syncFn.GetItems()
+	require.Len(t, items, 1)
+	require.Equal(t, asb, items[0])
 
 	// Test the case where the BackendSecurityPolicy is being deleted.
-	err = cl.Delete(t.Context(), &aigv1a1.BackendSecurityPolicy{ObjectMeta: metav1.ObjectMeta{Name: backendSecurityPolicyName, Namespace: namespace}})
+	err = fakeClient.Delete(t.Context(), &aigv1a1.BackendSecurityPolicy{ObjectMeta: metav1.ObjectMeta{Name: backendSecurityPolicyName, Namespace: namespace}})
 	require.NoError(t, err)
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: backendSecurityPolicyName}})
 	require.NoError(t, err)
 }
 
-// mockSTSOperations implements the STSOperations interface for testing
-type mockSTSOperations struct{}
+// mockSTSClient implements the STSOperations interface for testing
+type mockSTSClient struct{}
 
 // AssumeRoleWithWebIdentity will return placeholder of type aws credentials.
 //
-// This implements [STSClient.AssumeRoleWithWebIdentity].
-func (m *mockSTSOperations) AssumeRoleWithWebIdentity(_ context.Context, _ *sts.AssumeRoleWithWebIdentityInput, _ ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) {
+// This implements [rotators.STSClient.AssumeRoleWithWebIdentity].
+func (m *mockSTSClient) AssumeRoleWithWebIdentity(_ context.Context, _ *sts.AssumeRoleWithWebIdentityInput, _ ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) {
 	return &sts.AssumeRoleWithWebIdentityOutput{
 		Credentials: &stsTypes.Credentials{
 			AccessKeyId:     aws.String("NEWKEY"),
@@ -78,9 +101,9 @@ func (m *mockSTSOperations) AssumeRoleWithWebIdentity(_ context.Context, _ *sts.
 }
 
 func TestBackendSecurityPolicyController_ReconcileOIDC(t *testing.T) {
-	ch := make(chan ConfigSinkEvent, 100)
+	syncFn := internaltesting.NewSyncFnImpl[aigv1a1.AIServiceBackend]()
 	cl := fake.NewClientBuilder().WithScheme(scheme).Build()
-	c := newBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, ch)
+	c := newBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, syncFn.Sync)
 	backendSecurityPolicyName := "mybackendSecurityPolicy"
 	namespace := "default"
 
@@ -105,9 +128,8 @@ func TestBackendSecurityPolicyController_ReconcileOIDC(t *testing.T) {
 }
 
 func TestBackendSecurityController_RotateCredentials(t *testing.T) {
-	ch := make(chan ConfigSinkEvent, 100)
 	cl := fake.NewClientBuilder().WithScheme(scheme).Build()
-	c := newBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, ch)
+	c := newBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, internaltesting.NewSyncFnImpl[aigv1a1.AIServiceBackend]().Sync)
 	backendSecurityPolicyName := "mybackendSecurityPolicy"
 	namespace := "default"
 
@@ -177,7 +199,7 @@ func TestBackendSecurityController_RotateCredentials(t *testing.T) {
 	require.NoError(t, err)
 
 	ctx := oidcv3.InsecureIssuerURLContext(t.Context(), discoveryServer.URL)
-	rotator, err := rotators.NewAWSOIDCRotator(ctx, cl, &mockSTSOperations{}, fake2.NewClientset(), ctrl.Log, namespace, bsp.Name, preRotationWindow, "placeholder", "us-east-1")
+	rotator, err := rotators.NewAWSOIDCRotator(ctx, cl, &mockSTSClient{}, fake2.NewClientset(), ctrl.Log, namespace, bsp.Name, preRotationWindow, "placeholder", "us-east-1")
 	require.NoError(t, err)
 
 	res, err := c.rotateCredential(ctx, bsp, oidc, rotator)
diff --git a/internal/controller/secret_test.go b/internal/controller/secret_test.go
index 0fff7d9..9917e13 100644
--- a/internal/controller/secret_test.go
+++ b/internal/controller/secret_test.go
@@ -6,6 +6,7 @@
 package controller
 
 import (
+	"sort"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -14,32 +15,63 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	fake2 "k8s.io/client-go/kubernetes/fake"
 	ctrl "sigs.k8s.io/controller-runtime"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
+	internaltesting "github.com/envoyproxy/ai-gateway/internal/testing"
 )
 
 func TestSecretController_Reconcile(t *testing.T) {
-	ch := make(chan ConfigSinkEvent, 100)
-	cl := fake.NewClientBuilder().WithScheme(scheme).Build()
-	c := NewSecretController(cl, fake2.NewClientset(), ctrl.Log, ch)
+	syncFn := internaltesting.NewSyncFnImpl[aigv1a1.BackendSecurityPolicy]()
+	fakeClient := requireNewFakeClientWithIndexes(t)
+	c := NewSecretController(fakeClient, fake2.NewClientset(), ctrl.Log, syncFn.Sync)
 
-	err := cl.Create(t.Context(), &corev1.Secret{
+	err := fakeClient.Create(t.Context(), &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"},
 		StringData: map[string]string{"key": "value"},
 	})
 	require.NoError(t, err)
 
+	// Create a bsp that references the secret.
+	originals := []*aigv1a1.BackendSecurityPolicy{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type:   aigv1a1.BackendSecurityPolicyTypeAPIKey,
+				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: "mysecret"}},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "default"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
+				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
+					Region:          "us-west-2",
+					CredentialsFile: &aigv1a1.AWSCredentialsFile{SecretRef: &gwapiv1.SecretObjectReference{Name: "mysecret"}},
+				},
+			},
+		},
+	}
+	for _, bsp := range originals {
+		require.NoError(t, fakeClient.Create(t.Context(), bsp))
+	}
+
 	_, err = c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{
 		Namespace: "default", Name: "mysecret",
 	}})
 	require.NoError(t, err)
-
-	item, ok := <-ch
-	require.True(t, ok)
-	require.Equal(t, ConfigSinkEventSecretUpdate{Namespace: "default", Name: "mysecret"}, item)
+	actual := syncFn.GetItems()
+	sort.Slice(actual, func(i, j int) bool {
+		return actual[i].Name < actual[j].Name
+	})
+	sort.Slice(originals, func(i, j int) bool {
+		return originals[i].Name < originals[j].Name
+	})
+	require.Equal(t, originals, actual)
 
 	// Test the case where the Secret is being deleted.
-	err = cl.Delete(t.Context(), &corev1.Secret{
+	err = fakeClient.Delete(t.Context(), &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"},
 	})
 	require.NoError(t, err)
diff --git a/internal/controller/sink_test.go b/internal/controller/sink_test.go
deleted file mode 100644
index d223b90..0000000
--- a/internal/controller/sink_test.go
+++ /dev/null
@@ -1,952 +0,0 @@
-// Copyright Envoy AI Gateway Authors
-// SPDX-License-Identifier: Apache-2.0
-// The full text of the Apache license is available in the LICENSE file at
-// the root of the repo.
-
-package controller
-
-import (
-	"context"
-	"fmt"
-	"log/slog"
-	"os"
-	"strconv"
-	"testing"
-	"time"
-
-	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
-	"github.com/go-logr/logr"
-	"github.com/stretchr/testify/require"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	uuid2 "k8s.io/apimachinery/pkg/util/uuid"
-	"k8s.io/apimachinery/pkg/util/yaml"
-	fake2 "k8s.io/client-go/kubernetes/fake"
-	"k8s.io/utils/ptr"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
-	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
-
-	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
-	"github.com/envoyproxy/ai-gateway/filterapi"
-	"github.com/envoyproxy/ai-gateway/internal/controller/rotators"
-)
-
-func requireNewFakeClientWithIndexes(t *testing.T) client.Client {
-	builder := fake.NewClientBuilder().WithScheme(scheme)
-	err := applyIndexing(t.Context(), func(_ context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error {
-		builder = builder.WithIndex(obj, field, extractValue)
-		return nil
-	})
-	require.NoError(t, err)
-	return builder.Build()
-}
-
-func TestConfigSink_handleEvent(t *testing.T) {
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent)
-	s := newConfigSink(fakeClient, kube, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-	require.NotNil(t, s)
-
-	_, err := kube.CoreV1().ConfigMaps("ns").Create(t.Context(), &corev1.ConfigMap{
-		ObjectMeta: metav1.ObjectMeta{Name: "ai-eg-route-extproc-apple", Namespace: "ns"},
-	}, metav1.CreateOptions{})
-	require.NoError(t, err)
-
-	s.handleEvent(t.Context(), ConfigSinkEventSecretUpdate{Namespace: "ns", Name: "some-secret"})
-	s.handleEvent(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"}})
-	s.handleEvent(t.Context(), &aigv1a1.BackendSecurityPolicy{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"}})
-	s.handleEvent(t.Context(), &aigv1a1.AIGatewayRoute{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"}})
-}
-
-func TestConfigSink_syncAIGatewayRoute(t *testing.T) {
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent, 10)
-	s := newConfigSink(fakeClient, kube, logr.FromSlogHandler(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})), eventChan, "defaultExtProcImage", "debug")
-	require.NotNil(t, s)
-
-	for _, backend := range []*aigv1a1.AIServiceBackend{
-		{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns1"}, Spec: aigv1a1.AIServiceBackendSpec{
-			BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-		}},
-		{ObjectMeta: metav1.ObjectMeta{Name: "orange", Namespace: "ns1"}, Spec: aigv1a1.AIServiceBackendSpec{
-			BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-		}},
-	} {
-		err := fakeClient.Create(t.Context(), backend, &client.CreateOptions{})
-		require.NoError(t, err)
-	}
-
-	t.Run("existing", func(t *testing.T) {
-		route := &aigv1a1.AIGatewayRoute{
-			ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
-			Spec: aigv1a1.AIGatewayRouteSpec{
-				Rules: []aigv1a1.AIGatewayRouteRule{
-					{
-						BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "apple", Weight: 1}, {Name: "orange", Weight: 1}},
-					},
-				},
-				APISchema: aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"},
-			},
-		}
-		err := fakeClient.Create(t.Context(), route, &client.CreateOptions{})
-		require.NoError(t, err)
-		httpRoute := &gwapiv1.HTTPRoute{
-			ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1", Labels: map[string]string{managedByLabel: "envoy-ai-gateway"}},
-			Spec:       gwapiv1.HTTPRouteSpec{},
-		}
-		err = fakeClient.Create(t.Context(), httpRoute, &client.CreateOptions{})
-		require.NoError(t, err)
-
-		// Create the initial configmap.
-		_, err = kube.CoreV1().ConfigMaps(route.Namespace).Create(t.Context(), &corev1.ConfigMap{
-			ObjectMeta: metav1.ObjectMeta{Name: extProcName(route), Namespace: route.Namespace},
-		}, metav1.CreateOptions{})
-		require.NoError(t, err)
-
-		// Then sync, which should update the HTTPRoute.
-		s.syncAIGatewayRoute(t.Context(), route)
-		var updatedHTTPRoute gwapiv1.HTTPRoute
-		err = fakeClient.Get(t.Context(), client.ObjectKey{Name: "route1", Namespace: "ns1"}, &updatedHTTPRoute)
-		require.NoError(t, err)
-		require.Len(t, updatedHTTPRoute.Spec.Rules, 3) // 2 backends + 1 for the default rule.
-		require.Len(t, updatedHTTPRoute.Spec.Rules[0].BackendRefs, 1)
-		require.Equal(t, "some-backend1", string(updatedHTTPRoute.Spec.Rules[0].BackendRefs[0].BackendRef.Name))
-		require.Equal(t, "apple.ns1", updatedHTTPRoute.Spec.Rules[0].Matches[0].Headers[0].Value)
-		require.Equal(t, "some-backend2", string(updatedHTTPRoute.Spec.Rules[1].BackendRefs[0].BackendRef.Name))
-		require.Equal(t, "orange.ns1", updatedHTTPRoute.Spec.Rules[1].Matches[0].Headers[0].Value)
-		// Defaulting to the first backend.
-		require.Equal(t, "some-backend1", string(updatedHTTPRoute.Spec.Rules[2].BackendRefs[0].BackendRef.Name))
-		require.Equal(t, "/", *updatedHTTPRoute.Spec.Rules[2].Matches[0].Path.Value)
-	})
-
-	// Check the namespace has the default host rewrite filter.
-	var f egv1a1.HTTPRouteFilter
-	err := s.client.Get(t.Context(), client.ObjectKey{Name: hostRewriteHTTPFilterName, Namespace: "ns1"}, &f)
-	require.NoError(t, err)
-	require.Equal(t, hostRewriteHTTPFilterName, f.Name)
-}
-
-func TestConfigSink_syncAIServiceBackend(t *testing.T) {
-	eventChan := make(chan ConfigSinkEvent)
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	// Create the AI Gateway Route that references the backend.
-	route := &aigv1a1.AIGatewayRoute{
-		ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
-		Spec: aigv1a1.AIGatewayRouteSpec{
-			Rules: []aigv1a1.AIGatewayRouteRule{
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "apple", Weight: 1}},
-				},
-			},
-		},
-	}
-	require.NoError(t, fakeClient.Create(t.Context(), route, &client.CreateOptions{}))
-
-	s := newConfigSink(fakeClient, nil, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-	s.syncAIServiceBackend(t.Context(), &aigv1a1.AIServiceBackend{
-		ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns1"},
-		Spec: aigv1a1.AIServiceBackendSpec{
-			BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-		},
-	})
-}
-
-func TestConfigSink_syncBackendSecurityPolicy(t *testing.T) {
-	eventChan := make(chan ConfigSinkEvent)
-	fakeClient := requireNewFakeClientWithIndexes(t)
-
-	backend := aigv1a1.AIServiceBackend{
-		ObjectMeta: metav1.ObjectMeta{Name: "tomato", Namespace: "ns"},
-		Spec: aigv1a1.AIServiceBackendSpec{
-			BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-			BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "apple"},
-		},
-	}
-	require.NoError(t, fakeClient.Create(t.Context(), &backend, &client.CreateOptions{}))
-
-	s := newConfigSink(fakeClient, nil, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-	s.syncBackendSecurityPolicy(t.Context(), &aigv1a1.BackendSecurityPolicy{
-		ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
-	})
-}
-
-func Test_newHTTPRoute(t *testing.T) {
-	eventChan := make(chan ConfigSinkEvent)
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	s := newConfigSink(fakeClient, nil, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-	httpRoute := &gwapiv1.HTTPRoute{
-		ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
-		Spec:       gwapiv1.HTTPRouteSpec{},
-	}
-	aiGatewayRoute := &aigv1a1.AIGatewayRoute{
-		ObjectMeta: metav1.ObjectMeta{Name: "route1", Namespace: "ns1"},
-		Spec: aigv1a1.AIGatewayRouteSpec{
-			TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
-				{
-					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
-						Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
-					},
-				},
-			},
-			Rules: []aigv1a1.AIGatewayRouteRule{
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "apple", Weight: 100}},
-				},
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-						{Name: "orange", Weight: 100},
-						{Name: "apple", Weight: 100},
-						{Name: "pineapple", Weight: 100},
-					},
-				},
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "foo", Weight: 1}},
-				},
-			},
-		},
-	}
-	for _, backend := range []*aigv1a1.AIServiceBackend{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns1"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "orange", Namespace: "ns1"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns1"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns1"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns1")},
-			},
-		},
-	} {
-		err := s.client.Create(t.Context(), backend, &client.CreateOptions{})
-		require.NoError(t, err)
-	}
-	err := s.newHTTPRoute(t.Context(), httpRoute, aiGatewayRoute)
-	require.NoError(t, err)
-
-	expRules := []gwapiv1.HTTPRouteRule{
-		{
-			Matches: []gwapiv1.HTTPRouteMatch{
-				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "apple.ns1"}}},
-			},
-			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
-		},
-		{
-			Matches: []gwapiv1.HTTPRouteMatch{
-				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "orange.ns1"}}},
-			},
-			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
-		},
-		{
-			Matches: []gwapiv1.HTTPRouteMatch{
-				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "pineapple.ns1"}}},
-			},
-			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
-		},
-		{
-			Matches: []gwapiv1.HTTPRouteMatch{
-				{Headers: []gwapiv1.HTTPHeaderMatch{{Name: selectedBackendHeaderKey, Value: "foo.ns1"}}},
-			},
-			BackendRefs: []gwapiv1.HTTPBackendRef{{BackendRef: gwapiv1.BackendRef{BackendObjectReference: gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns1")}}}},
-		},
-	}
-	require.Len(t, httpRoute.Spec.Rules, 5) // 4 backends + 1 for the default rule.
-	for i, r := range httpRoute.Spec.Rules {
-		t.Run(fmt.Sprintf("rule-%d", i), func(t *testing.T) {
-			if i == 4 {
-				require.Equal(t, expRules[0].BackendRefs, r.BackendRefs)
-				require.NotNil(t, r.Matches[0].Path)
-				require.Equal(t, "/", *r.Matches[0].Path.Value)
-			} else {
-				require.Equal(t, expRules[i].Matches, r.Matches)
-				require.Equal(t, expRules[i].BackendRefs, r.BackendRefs)
-			}
-
-			// Each rule should have a host rewrite filter by default.
-			require.Len(t, r.Filters, 1)
-			require.Equal(t, gwapiv1.HTTPRouteFilterExtensionRef, r.Filters[0].Type)
-			require.NotNil(t, r.Filters[0].ExtensionRef)
-			require.Equal(t, hostRewriteHTTPFilterName, string(r.Filters[0].ExtensionRef.Name))
-		})
-	}
-}
-
-func Test_updateExtProcConfigMap(t *testing.T) {
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent)
-	s := newConfigSink(fakeClient, kube, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-	require.NoError(t, fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy"}}))
-	require.NoError(t, fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-2"}}))
-
-	for _, bsp := range []*aigv1a1.BackendSecurityPolicy{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-1", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
-				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
-					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-2", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
-				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
-					Region: "us-east-1",
-					CredentialsFile: &aigv1a1.AWSCredentialsFile{
-						SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-						Profile:   "default",
-					},
-				},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-3", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
-				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
-					Region:            "us-east-1",
-					OIDCExchangeToken: &aigv1a1.AWSOIDCExchangeToken{},
-				},
-			},
-		},
-	} {
-		err := fakeClient.Create(t.Context(), bsp, &client.CreateOptions{})
-		require.NoError(t, err)
-	}
-
-	for _, b := range []*aigv1a1.AIServiceBackend{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				APISchema: aigv1a1.VersionedAPISchema{
-					Name: aigv1a1.APISchemaAWSBedrock,
-				},
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "cat", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "pen", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-2"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "dog", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend5", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-3"},
-			},
-		},
-	} {
-		err := fakeClient.Create(t.Context(), b, &client.CreateOptions{})
-		require.NoError(t, err)
-	}
-	require.NotNil(t, s)
-
-	for _, tc := range []struct {
-		name  string
-		route *aigv1a1.AIGatewayRoute
-		exp   *filterapi.Config
-	}{
-		{
-			name: "basic",
-			route: &aigv1a1.AIGatewayRoute{
-				ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"},
-				Spec: aigv1a1.AIGatewayRouteSpec{
-					APISchema: aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"},
-					Rules: []aigv1a1.AIGatewayRouteRule{
-						{
-							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-								{Name: "apple", Weight: 1},
-								{Name: "pineapple", Weight: 2},
-							},
-							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}}},
-							},
-						},
-						{
-							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "cat", Weight: 1}},
-							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai"}}},
-							},
-						},
-						{
-							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-								{Name: "pen", Weight: 2},
-							},
-							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-2"}}},
-							},
-						},
-						{
-							BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-								{Name: "dog", Weight: 1},
-							},
-							Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-								{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-3"}}},
-							},
-						},
-					},
-					LLMRequestCosts: []aigv1a1.LLMRequestCost{
-						{
-							Type:        aigv1a1.LLMRequestCostTypeOutputToken,
-							MetadataKey: "output-token",
-						},
-						{
-							Type:        aigv1a1.LLMRequestCostTypeInputToken,
-							MetadataKey: "input-token",
-						},
-						{
-							Type:        aigv1a1.LLMRequestCostTypeTotalToken,
-							MetadataKey: "total-token",
-						},
-						{
-							Type:          aigv1a1.LLMRequestCostTypeCEL,
-							MetadataKey:   "cel-token",
-							CELExpression: ptr.To("model == 'cool_model' ?  input_tokens * output_tokens : total_tokens"),
-						},
-					},
-				},
-			},
-			exp: &filterapi.Config{
-				UUID:                     string(uuid2.NewUUID()),
-				Schema:                   filterapi.VersionedAPISchema{Name: filterapi.APISchemaOpenAI, Version: "v123"},
-				ModelNameHeaderKey:       aigv1a1.AIModelHeaderKey,
-				MetadataNamespace:        aigv1a1.AIGatewayFilterMetadataNamespace,
-				SelectedBackendHeaderKey: selectedBackendHeaderKey,
-				Rules: []filterapi.RouteRule{
-					{
-						Backends: []filterapi.Backend{
-							{Name: "apple.ns", Weight: 1, Schema: filterapi.VersionedAPISchema{Name: filterapi.APISchemaAWSBedrock}, Auth: &filterapi.BackendAuth{
-								APIKey: &filterapi.APIKeyAuth{
-									Filename: "/etc/backend_security_policy/rule0-backref0-some-backend-security-policy-1/apiKey",
-								},
-							}}, {Name: "pineapple.ns", Weight: 2},
-						},
-						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}},
-					},
-					{
-						Backends: []filterapi.Backend{{Name: "cat.ns", Weight: 1, Auth: &filterapi.BackendAuth{
-							APIKey: &filterapi.APIKeyAuth{
-								Filename: "/etc/backend_security_policy/rule1-backref0-some-backend-security-policy-1/apiKey",
-							},
-						}}},
-						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai"}},
-					},
-					{
-						Backends: []filterapi.Backend{{Name: "pen.ns", Weight: 2, Auth: &filterapi.BackendAuth{
-							AWSAuth: &filterapi.AWSAuth{
-								CredentialFileName: "/etc/backend_security_policy/rule2-backref0-some-backend-security-policy-2/credentials",
-								Region:             "us-east-1",
-							},
-						}}},
-						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-2"}},
-					},
-					{
-						Backends: []filterapi.Backend{{Name: "dog.ns", Weight: 1, Auth: &filterapi.BackendAuth{
-							AWSAuth: &filterapi.AWSAuth{
-								CredentialFileName: "/etc/backend_security_policy/rule3-backref0-some-backend-security-policy-3/credentials",
-								Region:             "us-east-1",
-							},
-						}}},
-						Headers: []filterapi.HeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai-3"}},
-					},
-				},
-				LLMRequestCosts: []filterapi.LLMRequestCost{
-					{Type: filterapi.LLMRequestCostTypeOutputToken, MetadataKey: "output-token"},
-					{Type: filterapi.LLMRequestCostTypeInputToken, MetadataKey: "input-token"},
-					{Type: filterapi.LLMRequestCostTypeTotalToken, MetadataKey: "total-token"},
-					{Type: filterapi.LLMRequestCostTypeCELExpression, MetadataKey: "cel-token", CELExpression: "model == 'cool_model' ?  input_tokens * output_tokens : total_tokens"},
-				},
-			},
-		},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			_, err := s.kube.CoreV1().ConfigMaps(tc.route.Namespace).Create(t.Context(), &corev1.ConfigMap{
-				ObjectMeta: metav1.ObjectMeta{Name: extProcName(tc.route), Namespace: tc.route.Namespace},
-			}, metav1.CreateOptions{})
-			require.NoError(t, err)
-
-			err = s.updateExtProcConfigMap(t.Context(), tc.route, tc.exp.UUID)
-			require.NoError(t, err)
-
-			cm, err := s.kube.CoreV1().ConfigMaps(tc.route.Namespace).Get(t.Context(), extProcName(tc.route), metav1.GetOptions{})
-			require.NoError(t, err)
-			require.NotNil(t, cm)
-
-			data := cm.Data[expProcConfigFileName]
-			var actual filterapi.Config
-			require.NoError(t, yaml.Unmarshal([]byte(data), &actual))
-			require.Equal(t, tc.exp, &actual)
-		})
-	}
-}
-
-func TestConfigSink_SyncExtprocDeployment(t *testing.T) {
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent)
-	s := newConfigSink(fakeClient, kube, logr.Discard(), eventChan, "envoyproxy/ai-gateway-extproc:foo", "debug")
-	err := fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy"}})
-	require.NoError(t, err)
-
-	for _, bsp := range []*aigv1a1.BackendSecurityPolicy{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-backend-security-policy-1", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
-				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
-					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				},
-			},
-		},
-	} {
-		require.NoError(t, fakeClient.Create(t.Context(), bsp, &client.CreateOptions{}))
-	}
-
-	for _, b := range []*aigv1a1.AIServiceBackend{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				APISchema: aigv1a1.VersionedAPISchema{
-					Name: aigv1a1.APISchemaAWSBedrock,
-				},
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "cat", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-backend-security-policy-1"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				BackendRef: gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-			},
-		},
-	} {
-		require.NoError(t, fakeClient.Create(t.Context(), b, &client.CreateOptions{}))
-	}
-	require.NotNil(t, s)
-
-	aiGatewayRoute := &aigv1a1.AIGatewayRoute{
-		ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"},
-		TypeMeta: metav1.TypeMeta{
-			Kind: "AIGatewayRoute", // aiGatewayRoute controller typically adds these type meta
-		},
-		Spec: aigv1a1.AIGatewayRouteSpec{
-			FilterConfig: &aigv1a1.AIGatewayFilterConfig{
-				Type: aigv1a1.AIGatewayFilterConfigTypeExternalProcess,
-				ExternalProcess: &aigv1a1.AIGatewayFilterConfigExternalProcess{
-					Replicas: ptr.To[int32](123),
-					Resources: &corev1.ResourceRequirements{
-						Limits: corev1.ResourceList{
-							corev1.ResourceCPU:    resource.MustParse("200m"),
-							corev1.ResourceMemory: resource.MustParse("100Mi"),
-						},
-					},
-				},
-			},
-			APISchema: aigv1a1.VersionedAPISchema{Name: aigv1a1.APISchemaOpenAI, Version: "v123"},
-			Rules: []aigv1a1.AIGatewayRouteRule{
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-						{Name: "apple", Weight: 1},
-						{Name: "pineapple", Weight: 2},
-					},
-					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}}},
-					},
-				},
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: "cat", Weight: 1}},
-					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "another-ai"}}},
-					},
-				},
-			},
-			TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
-				{
-					LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
-						Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
-					},
-				},
-			},
-		},
-	}
-
-	require.NoError(t, fakeClient.Create(t.Context(), aiGatewayRoute, &client.CreateOptions{}))
-
-	t.Run("create", func(t *testing.T) {
-		err = s.syncExtProcDeployment(t.Context(), aiGatewayRoute)
-		require.NoError(t, err)
-
-		resourceLimits := &corev1.ResourceRequirements{
-			Limits: corev1.ResourceList{
-				corev1.ResourceCPU:    resource.MustParse("200m"),
-				corev1.ResourceMemory: resource.MustParse("100Mi"),
-			},
-		}
-		require.Eventually(t, func() bool {
-			extProcDeployment, err := s.kube.AppsV1().Deployments("ns").Get(t.Context(), extProcName(aiGatewayRoute), metav1.GetOptions{})
-			if err != nil {
-				t.Logf("failed to get deployment %s: %v", extProcName(aiGatewayRoute), err)
-				return false
-			}
-			require.Equal(t, "envoyproxy/ai-gateway-extproc:foo", extProcDeployment.Spec.Template.Spec.Containers[0].Image)
-			require.Len(t, extProcDeployment.OwnerReferences, 1)
-			require.Equal(t, "myroute", extProcDeployment.OwnerReferences[0].Name)
-			require.Equal(t, "AIGatewayRoute", extProcDeployment.OwnerReferences[0].Kind)
-			require.Equal(t, int32(123), *extProcDeployment.Spec.Replicas)
-			require.Equal(t, resourceLimits, &extProcDeployment.Spec.Template.Spec.Containers[0].Resources)
-			return true
-		}, 30*time.Second, 200*time.Millisecond)
-
-		service, err := s.kube.CoreV1().Services("ns").Get(t.Context(), extProcName(aiGatewayRoute), metav1.GetOptions{})
-		require.NoError(t, err)
-		require.Equal(t, extProcName(aiGatewayRoute), service.Name)
-	})
-
-	t.Run("update", func(t *testing.T) {
-		// Update fields in resource again
-		// Doing it again should not fail and update the deployment.
-		newResourceLimits := &corev1.ResourceRequirements{
-			Limits: corev1.ResourceList{
-				corev1.ResourceCPU:    resource.MustParse("300m"),
-				corev1.ResourceMemory: resource.MustParse("32Mi"),
-			},
-		}
-		aiGatewayRoute.Spec.FilterConfig.ExternalProcess.Resources = newResourceLimits
-		aiGatewayRoute.Spec.FilterConfig.ExternalProcess.Replicas = ptr.To[int32](456)
-
-		require.NoError(t, s.syncExtProcDeployment(t.Context(), aiGatewayRoute))
-		// Check the deployment is updated.
-		require.Eventually(t, func() bool {
-			extProcDeployment, err := s.kube.AppsV1().Deployments("ns").Get(t.Context(), extProcName(aiGatewayRoute), metav1.GetOptions{})
-			if err != nil {
-				t.Logf("failed to get deployment %s: %v", extProcName(aiGatewayRoute), err)
-				return false
-			}
-			require.Equal(t, "envoyproxy/ai-gateway-extproc:foo", extProcDeployment.Spec.Template.Spec.Containers[0].Image)
-			require.Len(t, extProcDeployment.OwnerReferences, 1)
-			require.Equal(t, "myroute", extProcDeployment.OwnerReferences[0].Name)
-			require.Equal(t, "AIGatewayRoute", extProcDeployment.OwnerReferences[0].Kind)
-			require.Equal(t, int32(456), *extProcDeployment.Spec.Replicas)
-			require.Equal(t, newResourceLimits, &extProcDeployment.Spec.Template.Spec.Containers[0].Resources)
-
-			for _, v := range extProcDeployment.Spec.Template.Spec.Containers[0].VolumeMounts {
-				require.True(t, v.ReadOnly)
-			}
-			return true
-		}, 30*time.Second, 200*time.Millisecond)
-	})
-}
-
-func TestConfigSink_MountBackendSecurityPolicySecrets(t *testing.T) {
-	// Create simple case
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent)
-	s := newConfigSink(fakeClient, kube, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-	ctx, cancel := context.WithCancel(t.Context())
-	defer cancel()
-	err := s.init(ctx)
-	require.NoError(t, err)
-	require.NoError(t, fakeClient.Create(t.Context(), &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy"}}))
-
-	for _, secret := range []*corev1.Secret{
-		{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-1"}},
-		{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-2"}},
-		{ObjectMeta: metav1.ObjectMeta{Name: "some-secret-policy-3"}},
-		{ObjectMeta: metav1.ObjectMeta{Name: "aws-oidc-name"}},
-	} {
-		require.NoError(t, fakeClient.Create(t.Context(), secret, &client.CreateOptions{}))
-	}
-
-	for _, bsp := range []*aigv1a1.BackendSecurityPolicy{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-other-backend-security-policy-1", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
-				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
-					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-other-backend-security-policy-2", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAPIKey,
-				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{
-					SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-2", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "aws-oidc-name", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
-				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
-					OIDCExchangeToken: &aigv1a1.AWSOIDCExchangeToken{},
-					Region:            "us-east-1",
-				},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "some-other-backend-security-policy-aws", Namespace: "ns"},
-			Spec: aigv1a1.BackendSecurityPolicySpec{
-				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
-				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
-					CredentialsFile: &aigv1a1.AWSCredentialsFile{
-						SecretRef: &gwapiv1.SecretObjectReference{Name: "some-secret-policy-3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-						Profile:   "default",
-					},
-					Region: "us-east-1",
-				},
-			},
-		},
-	} {
-		require.NoError(t, fakeClient.Create(t.Context(), bsp, &client.CreateOptions{}))
-	}
-
-	for _, backend := range []*aigv1a1.AIServiceBackend{
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				APISchema: aigv1a1.VersionedAPISchema{
-					Name: aigv1a1.APISchemaAWSBedrock,
-				},
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-other-backend-security-policy-1"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "pineapple", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				APISchema: aigv1a1.VersionedAPISchema{
-					Name: aigv1a1.APISchemaAWSBedrock,
-				},
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend3", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-other-backend-security-policy-aws"},
-			},
-		},
-		{
-			ObjectMeta: metav1.ObjectMeta{Name: "dog", Namespace: "ns"},
-			Spec: aigv1a1.AIServiceBackendSpec{
-				APISchema: aigv1a1.VersionedAPISchema{
-					Name: aigv1a1.APISchemaAWSBedrock,
-				},
-				BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend4", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-				BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "aws-oidc-name"},
-			},
-		},
-	} {
-		require.NoError(t, fakeClient.Create(t.Context(), backend, &client.CreateOptions{}))
-		require.NotNil(t, s)
-	}
-
-	aiGateway := aigv1a1.AIGatewayRoute{
-		ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "ns"},
-		Spec: aigv1a1.AIGatewayRouteSpec{
-			Rules: []aigv1a1.AIGatewayRouteRule{
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-						{Name: "apple", Weight: 1},
-					},
-					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai"}}},
-					},
-				},
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-						{Name: "pineapple", Weight: 1},
-					},
-					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai-2"}}},
-					},
-				},
-				{
-					BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{
-						{Name: "dog", Weight: 1},
-					},
-					Matches: []aigv1a1.AIGatewayRouteRuleMatch{
-						{Headers: []gwapiv1.HTTPHeaderMatch{{Name: aigv1a1.AIModelHeaderKey, Value: "some-ai-3"}}},
-					},
-				},
-			},
-		},
-	}
-
-	spec := corev1.PodSpec{
-		Volumes: []corev1.Volume{
-			{
-				Name: "extproc-config",
-				VolumeSource: corev1.VolumeSource{
-					ConfigMap: &corev1.ConfigMapVolumeSource{
-						LocalObjectReference: corev1.LocalObjectReference{
-							Name: "extproc-config",
-						},
-					},
-				},
-			},
-		},
-		Containers: []corev1.Container{
-			{VolumeMounts: []corev1.VolumeMount{{Name: "extproc-config", MountPath: "some-path", ReadOnly: true}}},
-		},
-	}
-
-	require.NoError(t, fakeClient.Create(t.Context(), &aiGateway, &client.CreateOptions{}))
-
-	updatedSpec, err := s.mountBackendSecurityPolicySecrets(t.Context(), &spec, &aiGateway)
-	require.NoError(t, err)
-
-	require.Len(t, updatedSpec.Volumes, 4)
-	require.Len(t, updatedSpec.Containers[0].VolumeMounts, 4)
-	// API Key.
-	require.Equal(t, "some-secret-policy-1", updatedSpec.Volumes[1].VolumeSource.Secret.SecretName)
-	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-1", updatedSpec.Volumes[1].Name)
-	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-1", updatedSpec.Containers[0].VolumeMounts[1].Name)
-	require.Equal(t, "/etc/backend_security_policy/rule0-backref0-some-other-backend-security-policy-1", updatedSpec.Containers[0].VolumeMounts[1].MountPath)
-	// AWS CredentialFile.
-	require.Equal(t, "some-secret-policy-3", updatedSpec.Volumes[2].VolumeSource.Secret.SecretName)
-	require.Equal(t, "rule1-backref0-some-other-backend-security-policy-aws", updatedSpec.Volumes[2].Name)
-	require.Equal(t, "rule1-backref0-some-other-backend-security-policy-aws", updatedSpec.Containers[0].VolumeMounts[2].Name)
-	require.Equal(t, "/etc/backend_security_policy/rule1-backref0-some-other-backend-security-policy-aws", updatedSpec.Containers[0].VolumeMounts[2].MountPath)
-	// AWS OIDC.
-	require.Equal(t, rotators.GetBSPSecretName("aws-oidc-name"), updatedSpec.Volumes[3].VolumeSource.Secret.SecretName)
-	require.Equal(t, "rule2-backref0-aws-oidc-name", updatedSpec.Volumes[3].Name)
-	require.Equal(t, "rule2-backref0-aws-oidc-name", updatedSpec.Containers[0].VolumeMounts[3].Name)
-	require.Equal(t, "/etc/backend_security_policy/rule2-backref0-aws-oidc-name", updatedSpec.Containers[0].VolumeMounts[3].MountPath)
-
-	require.NoError(t, fakeClient.Delete(t.Context(), &aigv1a1.AIServiceBackend{ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"}}, &client.DeleteOptions{}))
-
-	// Update to new security policy.
-	backend := aigv1a1.AIServiceBackend{
-		ObjectMeta: metav1.ObjectMeta{Name: "apple", Namespace: "ns"},
-		Spec: aigv1a1.AIServiceBackendSpec{
-			APISchema: aigv1a1.VersionedAPISchema{
-				Name: aigv1a1.APISchemaAWSBedrock,
-			},
-			BackendRef:               gwapiv1.BackendObjectReference{Name: "some-backend1", Namespace: ptr.To[gwapiv1.Namespace]("ns")},
-			BackendSecurityPolicyRef: &gwapiv1.LocalObjectReference{Name: "some-other-backend-security-policy-2"},
-		},
-	}
-
-	require.NoError(t, fakeClient.Create(t.Context(), &backend, &client.CreateOptions{}))
-	require.NotNil(t, s)
-
-	updatedSpec, err = s.mountBackendSecurityPolicySecrets(t.Context(), &spec, &aiGateway)
-	require.NoError(t, err)
-
-	require.Len(t, updatedSpec.Volumes, 4)
-	require.Len(t, updatedSpec.Containers[0].VolumeMounts, 4)
-	require.Equal(t, "some-secret-policy-2", updatedSpec.Volumes[1].VolumeSource.Secret.SecretName)
-	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-2", updatedSpec.Volumes[1].Name)
-	require.Equal(t, "rule0-backref0-some-other-backend-security-policy-2", updatedSpec.Containers[0].VolumeMounts[1].Name)
-	require.Equal(t, "/etc/backend_security_policy/rule0-backref0-some-other-backend-security-policy-2", updatedSpec.Containers[0].VolumeMounts[1].MountPath)
-
-	for _, v := range updatedSpec.Containers[0].VolumeMounts {
-		require.True(t, v.ReadOnly, v.Name)
-	}
-}
-
-func Test_backendSecurityPolicyVolumeName(t *testing.T) {
-	mountPath := backendSecurityPolicyVolumeName(1, 2, "name")
-	require.Equal(t, "rule1-backref2-name", mountPath)
-}
-
-func Test_annotateExtProcPods(t *testing.T) {
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent)
-	s := newConfigSink(fakeClient, kube, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-
-	aiGatewayRoute := &aigv1a1.AIGatewayRoute{
-		ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: "foons"},
-	}
-
-	for i := range 5 {
-		pod := &corev1.Pod{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "somepod" + strconv.Itoa(i),
-				Namespace: "foons",
-				Labels:    map[string]string{"app": extProcName(aiGatewayRoute)},
-			},
-			Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "someapp"}}},
-		}
-		_, err := kube.CoreV1().Pods("foons").Create(t.Context(), pod, metav1.CreateOptions{})
-		require.NoError(t, err)
-	}
-
-	uuid := string(uuid2.NewUUID())
-	err := s.annotateExtProcPods(t.Context(), aiGatewayRoute, uuid)
-	require.NoError(t, err)
-
-	// Check that all pods have been annotated.
-	for i := range 5 {
-		pod, err := kube.CoreV1().Pods("foons").Get(t.Context(), "somepod"+strconv.Itoa(i), metav1.GetOptions{})
-		require.NoError(t, err)
-		require.Equal(t, uuid, pod.Annotations[extProcConfigAnnotationKey])
-	}
-}
-
-func Test_syncSecret(t *testing.T) {
-	fakeClient := requireNewFakeClientWithIndexes(t)
-	kube := fake2.NewClientset()
-
-	eventChan := make(chan ConfigSinkEvent)
-	s := newConfigSink(fakeClient, kube, logr.Discard(), eventChan, "defaultExtProcImage", "debug")
-
-	_, err := kube.CoreV1().Secrets("ns").Create(t.Context(), &corev1.Secret{
-		ObjectMeta: metav1.ObjectMeta{Name: "some-secret", Namespace: "ns"},
-		Data:       map[string][]byte{"key": []byte("value")},
-	}, metav1.CreateOptions{})
-	require.NoError(t, err)
-	s.syncSecret(t.Context(), "ns", "some-secret")
-}
diff --git a/internal/testing/testing.go b/internal/testing/testing.go
new file mode 100644
index 0000000..be602ab
--- /dev/null
+++ b/internal/testing/testing.go
@@ -0,0 +1,46 @@
+// Copyright Envoy AI Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package internaltesting
+
+import (
+	"context"
+	"sync"
+)
+
+// SyncFnImpl is a test implementation of the sync* functions in the controller package.
+type SyncFnImpl[T any] struct {
+	items []*T
+	mux   sync.Mutex
+}
+
+// NewSyncFnImpl creates a new SyncFnImpl.
+func NewSyncFnImpl[T any]() *SyncFnImpl[T] {
+	return &SyncFnImpl[T]{}
+}
+
+// Sync is the actual implementation of the sync function.
+func (s *SyncFnImpl[T]) Sync(_ context.Context, item *T) error {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	s.items = append(s.items, item)
+	return nil
+}
+
+// GetItems returns a copy of the items.
+func (s *SyncFnImpl[T]) GetItems() []*T {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	ret := make([]*T, len(s.items))
+	copy(ret, s.items)
+	return ret
+}
+
+// Reset resets the items.
+func (s *SyncFnImpl[T]) Reset() {
+	s.mux.Lock()
+	defer s.mux.Unlock()
+	s.items = s.items[:0]
+}
diff --git a/internal/testing/testing_test.go b/internal/testing/testing_test.go
new file mode 100644
index 0000000..1750452
--- /dev/null
+++ b/internal/testing/testing_test.go
@@ -0,0 +1,40 @@
+// Copyright Envoy AI Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package internaltesting
+
+import (
+	"sort"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestSyncFnImpl(t *testing.T) {
+	s := NewSyncFnImpl[int]()
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+	for i := range 2 {
+		go func(v int) {
+			defer wg.Done()
+			require.NoError(t, s.Sync(t.Context(), &v))
+		}(i)
+	}
+	wg.Wait()
+
+	items := s.GetItems()
+	require.Len(t, items, 2)
+	sort.Slice(items, func(i, j int) bool {
+		return *items[i] < *items[j]
+	})
+	require.Equal(t, 0, *items[0])
+	require.Equal(t, 1, *items[1])
+
+	s.Reset()
+	items = s.GetItems()
+	require.Empty(t, items)
+}
diff --git a/tests/controller/controller_test.go b/tests/controller/controller_test.go
index 4968fad..1b37070 100644
--- a/tests/controller/controller_test.go
+++ b/tests/controller/controller_test.go
@@ -14,6 +14,7 @@ import (
 	"fmt"
 	"log/slog"
 	"os"
+	"sort"
 	"testing"
 	"time"
 
@@ -25,7 +26,6 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/klog/v2"
 	"k8s.io/utils/ptr"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -35,6 +35,7 @@ import (
 
 	aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
 	"github.com/envoyproxy/ai-gateway/internal/controller"
+	internaltesting "github.com/envoyproxy/ai-gateway/internal/testing"
 	testsinternal "github.com/envoyproxy/ai-gateway/tests/internal"
 )
 
@@ -51,16 +52,11 @@ func extProcName(aiGatewayRouteName string) string {
 // TestStartControllers tests the [controller.StartControllers] function.
 func TestStartControllers(t *testing.T) {
 	c, cfg, k := testsinternal.NewEnvTest(t)
-	opts := controller.Options{
-		ExtProcImage:         "envoyproxy/ai-gateway-extproc:foo",
-		EnableLeaderElection: false,
-	}
-	l := logr.FromSlogHandler(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
-	klog.SetLogger(l)
+	opts := controller.Options{ExtProcImage: "envoyproxy/ai-gateway-extproc:foo", EnableLeaderElection: false}
 
 	ctx := t.Context()
 	go func() {
-		err := controller.StartControllers(ctx, cfg, l, opts)
+		err := controller.StartControllers(ctx, cfg, defaultLogger(), opts)
 		require.NoError(t, err)
 	}()
 
@@ -334,9 +330,8 @@ func TestStartControllers(t *testing.T) {
 
 func TestAIGatewayRouteController(t *testing.T) {
 	c, cfg, k := testsinternal.NewEnvTest(t)
-	ch := make(chan controller.ConfigSinkEvent)
 
-	rc := controller.NewAIGatewayRouteController(c, k, logr.Discard(), ch)
+	rc := controller.NewAIGatewayRouteController(c, k, defaultLogger(), "gcr.io/ai-gateway/extproc:latest", "info")
 
 	opt := ctrl.Options{Scheme: c.Scheme(), LeaderElection: false, Controller: config.Controller{SkipNameValidation: ptr.To(true)}}
 	mgr, err := ctrl.NewManager(cfg, opt)
@@ -345,9 +340,8 @@ func TestAIGatewayRouteController(t *testing.T) {
 	err = ctrl.NewControllerManagedBy(mgr).For(&aigv1a1.AIGatewayRoute{}).Complete(rc)
 	require.NoError(t, err)
 
-	ctx := t.Context()
 	go func() {
-		err := mgr.Start(ctx)
+		err := mgr.Start(t.Context())
 		require.NoError(t, err)
 	}()
 
@@ -389,20 +383,76 @@ func TestAIGatewayRouteController(t *testing.T) {
 			},
 		},
 	}
+
+	for _, b := range []string{"backend1", "backend2"} {
+		err := c.Create(t.Context(), &aigv1a1.AIServiceBackend{
+			ObjectMeta: metav1.ObjectMeta{Name: b, Namespace: "default"},
+			Spec: aigv1a1.AIServiceBackendSpec{
+				APISchema: defaultSchema,
+				BackendRef: gwapiv1.BackendObjectReference{
+					Name: gwapiv1.ObjectName(b),
+					Port: ptr.To[gwapiv1.PortNumber](8080),
+				},
+			},
+		})
+		require.NoError(t, err)
+	}
 	t.Run("create route", func(t *testing.T) {
-		err := c.Create(ctx, origin)
+		err := c.Create(t.Context(), origin)
 		require.NoError(t, err)
 
-		item, ok := <-ch
-		require.True(t, ok)
-		require.IsType(t, &aigv1a1.AIGatewayRoute{}, item)
+		var r aigv1a1.AIGatewayRoute
+		err = c.Get(t.Context(), client.ObjectKey{Name: "myroute", Namespace: "default"}, &r)
+		require.NoError(t, err)
+		require.Equal(t, origin, &r)
 
-		// Verify that they are the same.
-		created := item.(*aigv1a1.AIGatewayRoute)
-		require.Equal(t, "myroute", created.Name)
+		// Verify that the deployment, service, extension policy, and configmap are created.
+		require.Eventually(t, func() bool {
+			deployment, err := k.AppsV1().Deployments("default").Get(t.Context(), extProcName("myroute"), metav1.GetOptions{})
+			if err != nil {
+				t.Logf("failed to get deployment %s: %v", extProcName("myroute"), err)
+				return false
+			}
+			require.Equal(t, "gcr.io/ai-gateway/extproc:latest", deployment.Spec.Template.Spec.Containers[0].Image)
+			require.Len(t, deployment.OwnerReferences, 1)
+			require.Equal(t, origin.Name, deployment.OwnerReferences[0].Name)
+			require.Equal(t, "AIGatewayRoute", deployment.OwnerReferences[0].Kind)
+			require.True(t, *deployment.OwnerReferences[0].Controller)
+			require.Equal(t, int32(5), *deployment.Spec.Replicas)
+			require.Equal(t, resourceReq, &deployment.Spec.Template.Spec.Containers[0].Resources)
+
+			service, err := k.CoreV1().Services("default").Get(t.Context(), extProcName("myroute"), metav1.GetOptions{})
+			if err != nil {
+				t.Logf("failed to get service %s: %v", extProcName("myroute"), err)
+				return false
+			}
+			require.Equal(t, extProcName("myroute"), service.Name)
+			require.Len(t, service.OwnerReferences, 1)
+			require.Equal(t, origin.Name, service.OwnerReferences[0].Name)
+			require.Equal(t, "AIGatewayRoute", service.OwnerReferences[0].Kind)
+			require.True(t, *service.OwnerReferences[0].Controller)
+
+			extPolicy := egv1a1.EnvoyExtensionPolicy{}
+			err = c.Get(t.Context(), client.ObjectKey{Name: extProcName("myroute"), Namespace: "default"}, &extPolicy)
+			if err != nil {
+				t.Logf("failed to get extension policy %s: %v", extProcName("myroute"), err)
+				return false
+			}
+			require.Len(t, extPolicy.OwnerReferences, 1)
+			require.Equal(t, origin.Name, extPolicy.OwnerReferences[0].Name)
+			require.True(t, *extPolicy.OwnerReferences[0].Controller)
 
-		created.TypeMeta = metav1.TypeMeta{} // This will be populated by the controller internally, so we ignore it.
-		require.Equal(t, origin, created)
+			configMap, err := k.CoreV1().ConfigMaps("default").Get(t.Context(), extProcName("myroute"), metav1.GetOptions{})
+			if err != nil {
+				t.Logf("failed to get configmap %s: %v", extProcName("myroute"), err)
+				return false
+			}
+			require.Len(t, configMap.OwnerReferences, 1)
+			require.Equal(t, origin.Name, configMap.OwnerReferences[0].Name)
+			require.True(t, *configMap.OwnerReferences[0].Controller)
+			require.Contains(t, configMap.Data, "extproc-config.yaml")
+			return true
+		}, 30*time.Second, 200*time.Millisecond)
 	})
 
 	t.Run("update", func(t *testing.T) {
@@ -414,42 +464,102 @@ func TestAIGatewayRouteController(t *testing.T) {
 		}
 		origin.Spec.FilterConfig.ExternalProcess.Replicas = ptr.To[int32](3)
 		origin.Spec.FilterConfig.ExternalProcess.Resources = newResource
-		err := c.Update(ctx, origin)
+		err := c.Update(t.Context(), origin)
 		require.NoError(t, err)
 
-		item, ok := <-ch
-		require.True(t, ok)
-		require.IsType(t, &aigv1a1.AIGatewayRoute{}, item)
+		var r aigv1a1.AIGatewayRoute
+		err = c.Get(t.Context(), client.ObjectKey{Name: "myroute", Namespace: "default"}, &r)
+		require.NoError(t, err)
+		require.Equal(t, origin, &r)
 
-		// Verify that they are the same.
-		created := item.(*aigv1a1.AIGatewayRoute)
-		created.TypeMeta = metav1.TypeMeta{} // This will be populated by the controller internally, so we ignore it.
-		require.Equal(t, origin, created)
+		require.Eventually(t, func() bool {
+			deployment, err := k.AppsV1().Deployments("default").Get(t.Context(), extProcName("myroute"), metav1.GetOptions{})
+			if err != nil {
+				t.Logf("failed to get deployment %s: %v", extProcName("myroute"), err)
+				return false
+			}
+			require.Equal(t, "gcr.io/ai-gateway/extproc:latest", deployment.Spec.Template.Spec.Containers[0].Image)
+			require.Len(t, deployment.OwnerReferences, 1)
+			require.Equal(t, origin.Name, deployment.OwnerReferences[0].Name)
+			require.Equal(t, "AIGatewayRoute", deployment.OwnerReferences[0].Kind)
+			require.True(t, *deployment.OwnerReferences[0].Controller)
+			require.Equal(t, int32(3), *deployment.Spec.Replicas)
+			require.Equal(t, newResource, &deployment.Spec.Template.Spec.Containers[0].Resources)
+			return true
+		}, 30*time.Second, 200*time.Millisecond)
 	})
 }
 
+func TestBackendSecurityPolicyController(t *testing.T) { t.Skip("TODO") }
+
 func TestAIServiceBackendController(t *testing.T) {
 	c, cfg, k := testsinternal.NewEnvTest(t)
 
-	ch := make(chan controller.ConfigSinkEvent)
-	bc := controller.NewAIServiceBackendController(c, k, logr.Discard(), ch)
+	syncAIGatewayRoute := internaltesting.NewSyncFnImpl[aigv1a1.AIGatewayRoute]()
 
 	opt := ctrl.Options{Scheme: c.Scheme(), LeaderElection: false, Controller: config.Controller{SkipNameValidation: ptr.To(true)}}
 	mgr, err := ctrl.NewManager(cfg, opt)
 	require.NoError(t, err)
+	require.NoError(t, controller.ApplyIndexing(t.Context(), mgr.GetFieldIndexer().IndexField))
 
+	bc := controller.NewAIServiceBackendController(mgr.GetClient(), k, defaultLogger(), syncAIGatewayRoute.Sync)
 	err = ctrl.NewControllerManagedBy(mgr).For(&aigv1a1.AIServiceBackend{}).Complete(bc)
 	require.NoError(t, err)
 
-	ctx := t.Context()
 	go func() {
-		err := mgr.Start(ctx)
+		err := mgr.Start(t.Context())
 		require.NoError(t, err)
 	}()
 
+	const aiServiceBackendName, aiServiceBackendNamespace = "mybackend", "default"
+	// Create an AIGatewayRoute to be referenced by the AIServiceBackend.
+	originals := []*aigv1a1.AIGatewayRoute{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "myroute", Namespace: aiServiceBackendNamespace},
+			Spec: aigv1a1.AIGatewayRouteSpec{
+				APISchema: defaultSchema,
+				TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+					{
+						LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+							Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
+						},
+					},
+				},
+				Rules: []aigv1a1.AIGatewayRouteRule{
+					{
+						Matches:     []aigv1a1.AIGatewayRouteRuleMatch{{}},
+						BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: aiServiceBackendName}},
+					},
+				},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "myroute2", Namespace: aiServiceBackendNamespace},
+			Spec: aigv1a1.AIGatewayRouteSpec{
+				APISchema: defaultSchema,
+				TargetRefs: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{
+					{
+						LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{
+							Name: "gtw", Kind: "Gateway", Group: "gateway.networking.k8s.io",
+						},
+					},
+				},
+				Rules: []aigv1a1.AIGatewayRouteRule{
+					{
+						Matches:     []aigv1a1.AIGatewayRouteRuleMatch{{}},
+						BackendRefs: []aigv1a1.AIGatewayRouteRuleBackendRef{{Name: aiServiceBackendName}},
+					},
+				},
+			},
+		},
+	}
+	for _, route := range originals {
+		require.NoError(t, c.Create(t.Context(), route))
+	}
+
 	t.Run("create backend", func(t *testing.T) {
 		origin := &aigv1a1.AIServiceBackend{
-			ObjectMeta: metav1.ObjectMeta{Name: "mybackend", Namespace: "default"},
+			ObjectMeta: metav1.ObjectMeta{Name: aiServiceBackendName, Namespace: aiServiceBackendNamespace},
 			Spec: aigv1a1.AIServiceBackendSpec{
 				APISchema: defaultSchema,
 				BackendRef: gwapiv1.BackendObjectReference{
@@ -458,71 +568,132 @@ func TestAIServiceBackendController(t *testing.T) {
 				},
 			},
 		}
-		err := c.Create(ctx, origin)
+		err := c.Create(t.Context(), origin)
 		require.NoError(t, err)
 
-		item, ok := <-ch
-		require.True(t, ok)
-		require.IsType(t, &aigv1a1.AIServiceBackend{}, item)
+		require.Eventually(t, func() bool {
+			return len(syncAIGatewayRoute.GetItems()) == 2
+		}, 5*time.Second, 200*time.Millisecond)
 
 		// Verify that they are the same.
-		created := item.(*aigv1a1.AIServiceBackend)
-		require.Equal(t, origin, created)
+		routes := syncAIGatewayRoute.GetItems()
+		sort.Slice(routes, func(i, j int) bool {
+			routes[i].TypeMeta = metav1.TypeMeta{}
+			routes[j].TypeMeta = metav1.TypeMeta{}
+			return routes[i].Name < routes[j].Name
+		})
+		require.Equal(t, originals, routes)
+	})
+
+	syncAIGatewayRoute.Reset()
+	t.Run("update backend", func(t *testing.T) {
+		var origin aigv1a1.AIServiceBackend
+		err := c.Get(t.Context(), client.ObjectKey{Name: aiServiceBackendName, Namespace: aiServiceBackendNamespace}, &origin)
+		require.NoError(t, err)
+		origin.Spec.BackendRef.Port = ptr.To[gwapiv1.PortNumber](9090)
+		require.NoError(t, c.Update(t.Context(), &origin))
+
+		require.Eventually(t, func() bool {
+			return len(syncAIGatewayRoute.GetItems()) == 2
+		}, 5*time.Second, 200*time.Millisecond)
+
+		// Verify that they are the same.
+		routes := syncAIGatewayRoute.GetItems()
+		sort.Slice(routes, func(i, j int) bool {
+			routes[i].TypeMeta = metav1.TypeMeta{}
+			routes[j].TypeMeta = metav1.TypeMeta{}
+			return routes[i].Name < routes[j].Name
+		})
+		require.Equal(t, originals, routes)
 	})
 }
 
 func TestSecretController(t *testing.T) {
 	c, cfg, k := testsinternal.NewEnvTest(t)
 
-	ch := make(chan controller.ConfigSinkEvent)
-	sc := controller.NewSecretController(c, k, logr.Discard(), ch)
-
 	opt := ctrl.Options{Scheme: c.Scheme(), LeaderElection: false, Controller: config.Controller{SkipNameValidation: ptr.To(true)}}
 	mgr, err := ctrl.NewManager(cfg, opt)
 	require.NoError(t, err)
 
+	bspSyncFn := internaltesting.NewSyncFnImpl[aigv1a1.BackendSecurityPolicy]()
+	sc := controller.NewSecretController(mgr.GetClient(), k, defaultLogger(), bspSyncFn.Sync)
+	const secretName, secretNamespace = "mysecret", "default"
+
 	err = ctrl.NewControllerManagedBy(mgr).For(&corev1.Secret{}).Complete(sc)
 	require.NoError(t, err)
+	require.NoError(t, controller.ApplyIndexing(t.Context(), mgr.GetFieldIndexer().IndexField))
 
-	ctx := t.Context()
-	go func() {
-		err := mgr.Start(ctx)
-		require.NoError(t, err)
-	}()
+	go func() { require.NoError(t, mgr.Start(t.Context())) }()
 
-	t.Run("create secret", func(t *testing.T) {
-		origin := &corev1.Secret{
-			ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"},
-			StringData: map[string]string{
-				"key": "value",
+	// Create a bsp that references the secret.
+	originals := []*aigv1a1.BackendSecurityPolicy{
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "mybsp", Namespace: "default"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type:   aigv1a1.BackendSecurityPolicyTypeAPIKey,
+				APIKey: &aigv1a1.BackendSecurityPolicyAPIKey{SecretRef: &gwapiv1.SecretObjectReference{Name: secretName}},
 			},
-		}
-		err := c.Create(ctx, origin)
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{Name: "mybsp2", Namespace: "default"},
+			Spec: aigv1a1.BackendSecurityPolicySpec{
+				Type: aigv1a1.BackendSecurityPolicyTypeAWSCredentials,
+				AWSCredentials: &aigv1a1.BackendSecurityPolicyAWSCredentials{
+					Region:          "us-west-2",
+					CredentialsFile: &aigv1a1.AWSCredentialsFile{SecretRef: &gwapiv1.SecretObjectReference{Name: secretName}},
+				},
+			},
+		},
+	}
+	for _, bsp := range originals {
+		require.NoError(t, c.Create(t.Context(), bsp))
+	}
+	sort.Slice(originals, func(i, j int) bool { return originals[i].Name < originals[j].Name })
+
+	t.Run("create secret", func(t *testing.T) {
+		err := c.Create(t.Context(), &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: secretNamespace},
+			StringData: map[string]string{"key": "value"},
+		})
 		require.NoError(t, err)
 
-		item, ok := <-ch
-		require.True(t, ok)
-		event, ok := item.(controller.ConfigSinkEventSecretUpdate)
-		require.True(t, ok)
-		require.Equal(t, "default", event.Namespace)
-		require.Equal(t, "mysecret", event.Name)
+		require.Eventually(t, func() bool {
+			return len(bspSyncFn.GetItems()) == 2
+		}, 5*time.Second, 200*time.Millisecond)
+
+		// Verify that they are the same.
+		bsps := bspSyncFn.GetItems()
+		sort.Slice(bsps, func(i, j int) bool {
+			bsps[i].TypeMeta = metav1.TypeMeta{}
+			bsps[j].TypeMeta = metav1.TypeMeta{}
+			return bsps[i].Name < bsps[j].Name
+		})
+		require.Equal(t, originals, bsps)
 	})
 
+	bspSyncFn.Reset()
 	t.Run("update secret", func(t *testing.T) {
-		origin := &corev1.Secret{
+		err := c.Update(t.Context(), &corev1.Secret{
 			ObjectMeta: metav1.ObjectMeta{Name: "mysecret", Namespace: "default"},
-			StringData: map[string]string{
-				"key": "value2",
-			},
-		}
-		err := c.Update(ctx, origin)
+			StringData: map[string]string{"key": "value2"},
+		})
 		require.NoError(t, err)
 
-		item, ok := <-ch
-		require.True(t, ok)
-		event, ok := item.(controller.ConfigSinkEventSecretUpdate)
-		require.True(t, ok)
-		require.Equal(t, "default", event.Namespace)
-		require.Equal(t, "mysecret", event.Name)
+		require.Eventually(t, func() bool {
+			return len(bspSyncFn.GetItems()) == 2
+		}, 5*time.Second, 200*time.Millisecond)
+
+		bsps := bspSyncFn.GetItems()
+		// Verify that they are the same.
+		sort.Slice(bsps, func(i, j int) bool {
+			bsps[i].TypeMeta = metav1.TypeMeta{}
+			bsps[j].TypeMeta = metav1.TypeMeta{}
+			return bsps[i].Name < bsps[j].Name
+		})
+		require.Equal(t, originals, bsps)
 	})
 }
+
+func defaultLogger() logr.Logger {
+	return logr.FromSlogHandler(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
+}