<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="10" failures="2" skipped="14" tests="99" time="1175.931" timestamp="2026-11-01T20:41:13.606803+00:00" hostname="maas-group-test-rcz2w-e2e-maas-openshift-pod"><testcase classname="tests.test_aitenant_lifecycle.TestAITenantRevocationOperandsSerial" name="test_aitenant_delete_completes_after_cleanup_serviceaccount_removed" time="35.716" /><testcase classname="tests.test_echo_large_io.TestEchoFidelity" name="test_response_mirrors_prompt" time="4.842" /><testcase classname="tests.test_echo_large_io.TestEchoFidelity" name="test_max_tokens_does_not_truncate" time="0.415" /><testcase classname="tests.test_echo_large_io.TestLargeIO" name="test_large_prompt_round_trips_unmodified" time="0.256" /><testcase classname="tests.test_echo_large_io.TestLargeIO" name="test_response_size_tracks_prompt_size" time="0.422" /><testcase classname="tests.test_echo_large_io.TestCredentialLeak" name="test_api_key_not_echoed_in_response" time="0.144" /><testcase classname="tests.test_echo_large_io.TestCredentialLeak" name="test_canary_in_prompt_is_not_leaked_to_headers" time="10.887" /><testcase classname="tests.test_embedding_inference.TestEmbeddingDefaultDeny" name="test_embedding_default_deny_403" time="0.244" /><testcase classname="tests.test_embedding_inference.TestEmbeddingPathRouting" name="test_embedding_path_based_200" time="5.283" /><testcase classname="tests.test_embedding_inference.TestEmbeddingPathRouting" name="test_embedding_bbr_llmisvc_200" time="0.479" /><testcase classname="tests.test_embedding_inference.TestEmbeddingGovernance" name="test_embedding_trlp_429" time="16.599" /><testcase classname="tests.test_embedding_inference.TestEmbeddingGovernance" name="test_embedding_with_governance_200" time="26.461" /><testcase classname="tests.test_external_models.TestExternalModelDiscovery" name="test_maasmodelref_created" time="75.684"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelDiscovery" name="test_reconciler_created_httproute" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelDiscovery" name="test_reconciler_created_backend_service" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelAuth" name="test_missing_or_invalid_key_returns_401[invalid-key]" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelAuth" name="test_missing_or_invalid_key_returns_401[missing-key]" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelEgress" name="test_request_forwarded_returns_200" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelCleanup" name="test_delete_removes_httproute" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelPathRouting" name="test_wrong_path_returns_not_found" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestLegacyExternalModelMigration" name="test_migration_sets_legacy_status_and_removes_networking" time="1.263"><failure message="RuntimeError: `oc apply` failed for ExternalModel llm/e2e-legacy-migration-eabf40d2: Error from server (Forbidden): error when creating &quot;STDIN&quot;: admission webhook &quot;vexternalmodel.kb.io&quot; denied the request: maas.opendatahub.io ExternalModel is deprecated; create inference.opendatahub.io ExternalProvider and ExternalModel instead (see the External Model setup guide in the MaaS documentation)">self = &lt;test_external_models.TestLegacyExternalModelMigration object at 0x7fa89e6ed9d0&gt;

    def test_migration_sets_legacy_status_and_removes_networking(self):
        name = f"e2e-legacy-migration-{uuid.uuid4().hex[:8]}"
        secret_name = f"{name}-api-key"
        legacy_kind = "externalmodels.maas.opendatahub.io"
        legacy_resource_name = f"maas-{name}"
        expected_message = f"Model migrated to inference.opendatahub.io ExternalModel: {name}"
        deadline = time.monotonic() + 180
    
        try:
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {"name": secret_name, "namespace": MODEL_NAMESPACE},
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
            # Seed one legacy networking child so this test proves teardown ran,
            # even if IPP migrates the model before the legacy reconciler has
            # time to create the full networking set.
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Service",
                "metadata": {"name": legacy_resource_name, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "type": "ExternalName",
                    "externalName": EXTERNAL_ENDPOINT,
                    "ports": [{"name": "https", "port": 443}],
                },
            })
&gt;           _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": name, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "targetModel": TARGET_MODEL,
                    "endpoint": EXTERNAL_ENDPOINT,
                    "credentialRef": {"name": secret_name},
                },
            })

test/e2e/tests/test_external_models.py:485: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

cr_dict = {'apiVersion': 'maas.opendatahub.io/v1alpha1', 'kind': 'ExternalModel', 'metadata': {'name': 'e2e-legacy-migration-eab...el': 'gpt-3.5-turbo', 'endpoint': 'httpbingo.org', 'credentialRef': {'name': 'e2e-legacy-migration-eabf40d2-api-key'}}}

    def _apply_cr(cr_dict):
        result = _run_oc(["oc", "apply", "-f", "-"], input_text=json.dumps(cr_dict))
        if result.returncode != 0:
            meta = cr_dict.get("metadata", {})
&gt;           raise RuntimeError(
                f"`oc apply` failed for {cr_dict.get('kind')} "
                f"{meta.get('namespace', '&lt;cluster&gt;')}/{meta.get('name')}: "
                f"{result.stderr.strip() or result.stdout.strip()}"
            )
E           RuntimeError: `oc apply` failed for ExternalModel llm/e2e-legacy-migration-eabf40d2: Error from server (Forbidden): error when creating "STDIN": admission webhook "vexternalmodel.kb.io" denied the request: maas.opendatahub.io ExternalModel is deprecated; create inference.opendatahub.io ExternalProvider and ExternalModel instead (see the External Model setup guide in the MaaS documentation)

test/e2e/tests/test_helper.py:894: RuntimeError</failure></testcase><testcase classname="tests.test_external_models.TestExternalModelBodyRouting" name="test_unresolvable_model_body_does_not_error[wrong-model]" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_external_models.TestExternalModelBodyRouting" name="test_unresolvable_model_body_does_not_error[missing-model]" time="0.000"><error message="failed on setup with &quot;TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)&quot;">gateway_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev'
headers = {'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkN0QUgzN3Q4YkdNbnVhZjkzSEVQWXE1MG9lY2dxeEZxM2NDSzI1MTY3QWcifQ.e...KQi31gYNLoTQ7CdGCrBTC1bIpCH7N-H9jNMsz2a1keE21WDg_MuHIlVsY6cqZCaOFeJlRW1SOnRIN9RlA', 'Content-Type': 'application/json'}
api_keys_base_url = 'https://maas.apps.4ec91afccb26205cb961.prod.konflux-ocp-ci.dev/maas-api/v1/api-keys'

    @pytest.fixture(scope="module")
    def external_models_setup(gateway_url, headers, api_keys_base_url):
        """
        Create a single ExternalModel CR, MaaSModelRef, AuthPolicy, and
        Subscription pointing to an external endpoint. Cleanup after tests.
        """
        log.info(f"Setting up external model test fixture (endpoint: {EXTERNAL_ENDPOINT})...")
    
        try:
            # Create a dummy secret (ExternalModel requires credentialRef).
            # The apikey-injection plugin's secret store only watches Secrets carrying
            # this label; without it credential lookups silently fail with a 500 at
            # request time (masked by tests that only assert non-401/403).
            _apply_cr({
                "apiVersion": "v1",
                "kind": "Secret",
                "metadata": {
                    "name": f"{EXTERNAL_MODEL_NAME}-api-key",
                    "namespace": MODEL_NAMESPACE,
                    "labels": {"inference.llm-d.ai/ipp-managed": "true"},
                },
                "type": "Opaque",
                "stringData": {"api-key": "e2e-test-key"},
            })
    
            # Create ExternalProvider CR (inference.opendatahub.io — canonical)
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalProvider",
                "metadata": {"name": EXTERNAL_PROVIDER_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "provider": "openai",
                    "endpoint": EXTERNAL_ENDPOINT,
                    "auth": {
                        "type": "simple",
                        "secretRef": {"name": f"{EXTERNAL_MODEL_NAME}-api-key"},
                    },
                },
            })
    
            # Create ExternalModel CR (inference.opendatahub.io — canonical). This is
            # what the IPP model-provider-resolver plugin watches to populate its
            # model-&gt;provider store, and what maas-controller's externalModelHandler
            # tries first when reconciling the MaaSModelRef.
            _apply_cr({
                "apiVersion": "inference.opendatahub.io/v1alpha1",
                "kind": "ExternalModel",
                "metadata": {"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE},
                "spec": {
                    "externalProviderRefs": [
                        {
                            "ref": {"name": EXTERNAL_PROVIDER_NAME},
                            "targetModel": TARGET_MODEL,
                            "apiFormat": "openai-chat",
                            "path": "/v1/chat/completions",
                        },
                    ],
                },
            })
    
            # Create MaaSModelRef
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSModelRef",
                "metadata": {
                    "name": EXTERNAL_MODEL_NAME,
                    "namespace": MODEL_NAMESPACE,
                    "annotations": {
                        "maas.opendatahub.io/endpoint": EXTERNAL_ENDPOINT,
                        "maas.opendatahub.io/provider": "openai",
                    },
                },
                "spec": {
                    "modelRef": {"kind": "ExternalModel", "name": EXTERNAL_MODEL_NAME},
                },
            })
    
            # Create MaaSAuthPolicy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": EXTERNAL_AUTH_POLICY, "namespace": _subscription_ns()},
                "spec": {
                    "modelRefs": [{"name": EXTERNAL_MODEL_NAME, "namespace": MODEL_NAMESPACE}],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Create MaaSSubscription
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": EXTERNAL_SUBSCRIPTION, "namespace": _subscription_ns()},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [
                        {
                            "name": EXTERNAL_MODEL_NAME,
                            "namespace": MODEL_NAMESPACE,
                            "tokenRateLimits": [{"limit": 10000, "window": "1h"}],
                        },
                    ],
                },
            })
    
            # Wait for CRs to reconcile
            _wait_for_maas_auth_policy_phase(EXTERNAL_AUTH_POLICY, namespace=_subscription_ns())
            _wait_for_maas_subscription_phase(EXTERNAL_SUBSCRIPTION, namespace=_subscription_ns())
    
            # MaaSAuthPolicy phase Active only proves the gateway's fixed-size,
            # shared Kuadrant AuthPolicy (maas-gateway-auth) is Enforced — that
            # singleton doesn't change when a MaaSAuthPolicy is added, so it says
            # nothing about this ExternalModel's own HTTPRoute. Wait for the
            # HTTPRoute itself to be Accepted by the gateway, otherwise
            # TestExternalModelAuth's requests can race the data plane and get a
            # plain 404 ("no route") instead of a proper 401/403.
&gt;           _wait_for_httproute_accepted(EXTERNAL_MODEL_HTTPROUTE_NAME, namespace=MODEL_NAMESPACE)

test/e2e/tests/test_external_models.py:257: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

name = 'e2e-external-model', namespace = 'llm', timeout = 60

    def _wait_for_httproute_accepted(name, namespace=MODEL_NAMESPACE, timeout=60):
        """Wait for an HTTPRoute to be Accepted by at least one parent Gateway.
    
        The HTTPRoute object existing in the API server only proves the
        reconciler created it — the Gateway controller still needs to program it
        into the data plane before real HTTP traffic will match it. Requests
        sent before that happens race the gateway and get a plain 404 ("no
        route"), which is easy to mistake for an auth failure since callers
        usually assert on 401/403. Waiting on the gateway-level AuthPolicy
        Enforced condition does NOT prove this route is ready — that policy is
        a fixed-size singleton unrelated to any single HTTPRoute.
    
        Args:
            name: Name of the HTTPRoute
            namespace: Namespace (default: MODEL_NAMESPACE)
            timeout: Maximum wait time in seconds (default: 60)
    
        Returns:
            The HTTPRoute CR dict once Accepted
    
        Raises:
            TimeoutError: If no parent reports Accepted=True within timeout
        """
        deadline = time.time() + timeout
        log.info(f"Waiting for HTTPRoute {namespace}/{name} to be Accepted (timeout: {timeout}s)...")
    
        while time.time() &lt; deadline:
            cr = _get_cr("httproute", name, namespace)
            if cr:
                parents = (cr.get("status") or {}).get("parents") or []
                for parent in parents:
                    if any(
                        c.get("type") == "Accepted" and c.get("status") == "True"
                        for c in parent.get("conditions") or []
                    ):
                        log.info(f"HTTPRoute {namespace}/{name} is Accepted")
                        return cr
                log.debug(f"HTTPRoute {namespace}/{name}: no parent Accepted=True yet ({len(parents)} parent(s))")
            time.sleep(2)
    
        cr = _get_cr("httproute", name, namespace)
        parents = (cr.get("status") or {}).get("parents") if cr else None
&gt;       raise TimeoutError(
            f"HTTPRoute {namespace}/{name} did not report Accepted=True within {timeout}s "
            f"(parents={parents!r})"
        )
E       TimeoutError: HTTPRoute llm/e2e-external-model did not report Accepted=True within 60s (parents=None)

test/e2e/tests/test_helper.py:2147: TimeoutError</error></testcase><testcase classname="tests.test_gateway_filter_chain.TestGatewayFilterChain" name="test_no_listener_rejected" time="0.437"><skipped type="pytest.skip" message="default tenant runs praxis-extproc; ai-gateway-controller owns its EnvoyFilter">/workspace/source/test/e2e/tests/test_gateway_filter_chain.py:296: default tenant runs praxis-extproc; ai-gateway-controller owns its EnvoyFilter</skipped></testcase><testcase classname="tests.test_gateway_filter_chain.TestGatewayFilterChain" name="test_epp_runs_after_payload_processing" time="0.000"><skipped type="pytest.skip" message="default tenant runs praxis-extproc; ai-gateway-controller owns its EnvoyFilter">/workspace/source/test/e2e/tests/test_gateway_filter_chain.py:300: default tenant runs praxis-extproc; ai-gateway-controller owns its EnvoyFilter</skipped></testcase><testcase classname="tests.test_gateway_filter_chain.TestEndpointPickerTraffic" name="test_body_routed_requests_reach_the_endpoint_picker" time="0.413"><skipped type="pytest.skip" message="default tenant runs praxis-extproc; ai-gateway-controller owns its EnvoyFilter">/workspace/source/test/e2e/tests/test_gateway_filter_chain.py:265: default tenant runs praxis-extproc; ai-gateway-controller owns its EnvoyFilter</skipped></testcase><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_objective_created_and_status_ready" time="48.185" /><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_priority_update_propagates" time="0.652" /><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_manual_edit_is_reverted" time="0.559" /><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_gateway_authpolicy_sets_flow_control_headers" time="0.154" /><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_inference_with_spoofed_headers_succeeds" time="0.268" /><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_clearing_priority_deletes_objective" time="0.973" /><testcase classname="tests.test_inference_priority.TestInferencePriority" name="test_subscription_delete_removes_objective" time="12.579" /><testcase classname="tests.test_inference_priority.TestInferencePriorityWithoutScheduler" name="test_model_without_pool_is_not_applicable" time="14.351" /><testcase classname="tests.test_models_endpoint.TestModelsEndpoint" name="test_explicit_subscription_header" time="1.723" /><testcase classname="tests.test_models_endpoint.TestModelsEndpoint" name="test_models_filtered_by_subscription" time="1.363" /><testcase classname="tests.test_models_endpoint.TestModelsEndpoint" name="test_different_modelrefs_same_model_id" time="47.204" /><testcase classname="tests.test_models_endpoint.TestModelsEndpoint" name="test_empty_model_list" time="13.135" /><testcase classname="tests.test_models_endpoint.TestModelsEndpoint" name="test_central_models_endpoint_exempt_from_rate_limiting" time="18.242" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_per_tenant_ipp_deployments_exist" time="76.294" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_per_tenant_ipp_env_vars" time="0.283"><skipped type="pytest.skip" message="praxis-extproc; Go IPP GATEWAY_NAME/TENANT_NAMESPACE env not applicable">/workspace/source/test/e2e/tests/test_per_tenant_ipp_isolation.py:235: praxis-extproc; Go IPP GATEWAY_NAME/TENANT_NAMESPACE env not applicable</skipped></testcase><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_per_tenant_envoyfilter_workload_selector_isolated" time="0.474" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_per_tenant_envoyfilter_grpc_clusters" time="0.285"><skipped type="pytest.skip" message="praxis-extproc; EnvoyFilter uses payload-*-extproc clusters (see test_per_tenant_praxis_isolation)">/workspace/source/test/e2e/tests/test_per_tenant_ipp_isolation.py:266: praxis-extproc; EnvoyFilter uses payload-*-extproc clusters (see test_per_tenant_praxis_isolation)</skipped></testcase><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_default_tenant_keeps_legacy_ipp_names" time="0.278" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_multiple_tenant_ipp_stacks_coexist" time="0.432" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPInfrastructure" name="test_per_tenant_networkpolicy_when_applied" time="0.276" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPRouting" name="test_default_gateway_hits_default_ipp_only" time="3.527" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPRouting" name="test_tenant_gateway_hits_tenant_ipp_only" time="35.457" /><testcase classname="tests.test_per_tenant_ipp_isolation.TestPerTenantIPPCleanup" name="test_ipp_resources_removed_on_aitenant_delete" time="78.826" /><testcase classname="tests.test_praxis_extproc_restart.TestPraxisExtProcRestart" name="test_pre_processing_rollout_recovers_inference" time="14.452" /><testcase classname="tests.test_praxis_extproc_restart.TestPraxisExtProcRestart" name="test_processing_rollout_recovers_inference" time="14.051" /><testcase classname="tests.test_praxis_extproc_restart.TestPraxisExtProcRestart" name="test_both_extproc_rollout_recovers_inference" time="14.523" /><testcase classname="tests.test_praxis_extproc_secret_rotation.TestPraxisExtProcSecretRotation" name="test_in_place_secret_rotation_keeps_egress_without_extproc_restart" time="95.475" /><testcase classname="tests.test_subscription.TestSubscriptionEnforcement" name="test_auth_pass_no_subscription_gets_403" time="0.970" /><testcase classname="tests.test_subscription.TestSubscriptionEnforcement" name="test_rate_limit_exhaustion_gets_429" time="17.469" /><testcase classname="tests.test_subscription.TestMultipleSubscriptionsPerModel" name="test_user_in_one_of_two_subscriptions_gets_200" time="10.633" /><testcase classname="tests.test_subscription.TestUnlimitedSubscription" name="test_unlimited_and_token_rate_limits_are_mutually_exclusive" time="0.486" /><testcase classname="tests.test_subscription.TestUnlimitedSubscription" name="test_unlimited_key_is_not_rate_limited" time="8.273" /><testcase classname="tests.test_subscription.TestUnlimitedSubscription" name="test_unlimited_subscriptions_share_one_wasm_limit" time="0.150"><skipped type="pytest.skip" message="no Kuadrant WasmPlugin for gateway maas-default-gateway">/workspace/source/test/e2e/tests/test_subscription.py:828: no Kuadrant WasmPlugin for gateway maas-default-gateway</skipped></testcase><testcase classname="tests.test_subscription.TestUnlimitedSubscription" name="test_unlimited_usage_is_metered" time="24.924" /><testcase classname="tests.test_subscription.TestAllUnlimitedModel" name="test_all_unlimited_model_is_not_denied" time="19.916" /><testcase classname="tests.test_subscription.TestMultipleAuthPoliciesPerModel" name="test_two_auth_policies_or_logic" time="16.073" /><testcase classname="tests.test_subscription.TestMultipleAuthPoliciesPerModel" name="test_delete_one_auth_policy_other_still_works" time="1.051" /><testcase classname="tests.test_subscription.TestCascadeDeletion" name="test_delete_subscription_rebuilds_trlp" time="14.201" /><testcase classname="tests.test_subscription.TestCascadeDeletion" name="test_trlp_persists_during_multi_subscription_deletion" time="28.991" /><testcase classname="tests.test_subscription.TestCascadeDeletion" name="test_delete_last_subscription_denies_access" time="17.407" /><testcase classname="tests.test_subscription.TestCascadeDeletion" name="test_unconfigured_model_denied_by_gateway_auth" time="0.683" /><testcase classname="tests.test_subscription.TestRateLimitIdentityGenerations" name="test_legacy_and_short_identities_coexist" time="27.281" /><testcase classname="tests.test_subscription.TestManagedAnnotation" name="test_authpolicy_managed_false_prevents_update" time="0.288"><skipped type="pytest.skip" message="gateway-only mode: per-model AuthPolicy is not created">/workspace/source/test/e2e/tests/test_subscription.py:1615: gateway-only mode: per-model AuthPolicy is not created</skipped></testcase><testcase classname="tests.test_subscription.TestManagedAnnotation" name="test_trlp_managed_false_prevents_update" time="2.504" /><testcase classname="tests.test_subscription.TestE2ESubscriptionFlow" name="test_e2e_with_access_but_no_subscription_gets_403" time="15.603" /><testcase classname="tests.test_subscription.TestE2ESubscriptionFlow" name="test_e2e_single_subscription_auto_selects" time="119.546" /><testcase classname="tests.test_subscription.TestE2ESubscriptionFlow" name="test_e2e_group_based_auth_but_no_subscription_gets_403" time="15.328" /><testcase classname="tests.test_subscription.TestDegradedSubscriptionFiltering" name="test_failed_subscription_blocks_inference" time="16.592"><failure message="AssertionError: Expected access denied message, got: {&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_tra&#10;assert ('denied' in '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;today it &quot;}]}' or 'access' in '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;today it &quot;}]}')&#10; +  where '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;today it &quot;}]}' = &lt;built-in method lower of str object at 0x7fa89e56cea0&gt;()&#10; +    where &lt;built-in method lower of str object at 0x7fa89e56cea0&gt; = '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;Today it &quot;}]}'.lower&#10; +      where '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;Today it &quot;}]}' = &lt;Response [200]&gt;.text&#10; +  and   '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;today it &quot;}]}' = &lt;built-in method lower of str object at 0x7fa89e56d290&gt;()&#10; +    where &lt;built-in method lower of str object at 0x7fa89e56d290&gt; = '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;Today it &quot;}]}'.lower&#10; +      where '{&quot;id&quot;:&quot;cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3&quot;,&quot;created&quot;:1793566696,&quot;model&quot;:&quot;facebook/opt-125m&quot;,&quot;usage&quot;:{&quot;prompt_tokens&quot;:1,&quot;completion_tokens&quot;:2,&quot;total_tokens&quot;:3},&quot;object&quot;:&quot;text_completion&quot;,&quot;kv_transfer_params&quot;:null,&quot;choices&quot;:[{&quot;index&quot;:0,&quot;finish_reason&quot;:&quot;stop&quot;,&quot;text&quot;:&quot;Today it &quot;}]}' = &lt;Response [200]&gt;.text">self = &lt;test_subscription.TestDegradedSubscriptionFiltering object at 0x7fa89e6c5550&gt;

    @pytest.mark.serial
    def test_failed_subscription_blocks_inference(self):
        """
        Test: Failed subscription blocks inference via OPA rule.
    
        Setup:
        1. Create subscription with valid model (starts Active)
        2. Create API key
        3. Manually patch subscription to Failed phase
        4. Verify inference is rejected by OPA (403)
    
        Note: We use manual patching because naturally creating a Failed subscription
        requires only invalid models, which don't have routes (404 before OPA runs).
        """
        ns = _ns()
        subscription_name = "e2e-failed-sub-inf"
        auth_name = "e2e-failed-sub-inf-auth"
        sa_name = "e2e-failed-sub-inf-sa"
    
        try:
            oc_token = _create_sa_token(sa_name, namespace=MODEL_NAMESPACE)
            sa_user = _sa_to_user(sa_name, namespace=MODEL_NAMESPACE)
    
            # Create auth policy for valid model
            _create_test_auth_policy(auth_name, MODEL_REF, users=[sa_user])
    
            # Create subscription with valid model (will be Active)
            _create_test_subscription(subscription_name, MODEL_REF, users=[sa_user])
    
            cr = _wait_for_maas_subscription_phase(subscription_name, "Active")
    
            # This test overrides controller-owned status to exercise the gateway
            # rejection path. Let initial TRLP/discovery reconciliation settle first,
            # and run serially so a queued reconcile from parallel test activity does
            # not overwrite the injected Failed phase before the inference request.
            _wait_for_subscription_inference_ready(
                subscription_name,
                MODEL_REF,
                namespace=ns,
                model_namespace=MODEL_NAMESPACE,
            )
    
            # Verify it starts as Active
            phase = cr.get("status", {}).get("phase")
            log.info(f"Initial phase: {phase}")
            assert phase == "Active", f"Expected Active initially, got {phase}"
    
            # Create API key while Active
            api_key = _create_api_key(
                oc_token,
                name="failed-sub-test",
                subscription=subscription_name
            )
    
            # Verify inference works while Active
            log.info("Testing inference while Active...")
            r = _inference(api_key, path=MODEL_PATH, model_name=MODEL_NAME)
            assert r.status_code == 200,                 f"Expected 200 while Active, got {r.status_code}: {r.text[:200]}"
            log.info("✅ Inference works with Active subscription")
    
            # Manually patch subscription to Failed phase
            import subprocess
            import json
            from datetime import datetime
            log.info("Manually patching subscription to Failed phase...")
            patch_data = {
                "status": {
                    "phase": "Failed",
                    "conditions": [
                        {
                            "type": "Ready",
                            "status": "False",
                            "reason": "Failed",
                            "message": "Subscription failed",
                            "lastTransitionTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
                        }
                    ],
                    "modelRefStatuses": [
                        {
                            "name": MODEL_REF,
                            "namespace": MODEL_NAMESPACE,
                            "ready": False,
                            "reason": "ReconcileFailed",
                            "message": "Model failed"
                        }
                    ]
                }
            }
    
            cmd = [
                "kubectl", "patch", "maassubscription", subscription_name,
                "-n", ns,
                "--type=merge",
                "--subresource=status",
                "-p", json.dumps(patch_data),
                "-o", "json",
            ]
            result = subprocess.run(cmd, capture_output=True, text=True)
            assert result.returncode == 0, f"Failed to patch to Failed phase: {result.stderr}"
            # Read the object returned by the patch itself; a second API request
            # leaves extra time for any queued controller reconciliation to win.
            patched_cr = json.loads(result.stdout)
            phase = patched_cr.get("status", {}).get("phase")
            assert phase == "Failed", f"Expected Failed phase after patch, got {phase}"
            log.info("✅ Subscription patched to Failed phase")
    
            # Test inference with Failed subscription - should be rejected by OPA
            log.info("Testing inference with Failed subscription...")
            r = _inference(api_key, path=MODEL_PATH, model_name=MODEL_NAME)
    
            log.info(f"Response: status={r.status_code}, body={r.text[:200]}")
    
            # Failed phase should be rejected by OPA rule (403 or error message)
            if r.status_code == 200:
&gt;               assert "denied" in r.text.lower() or "access" in r.text.lower(),                     f"Expected access denied message, got: {r.text[:200]}"
E               AssertionError: Expected access denied message, got: {"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_tra
E               assert ('denied' in '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"today it "}]}' or 'access' in '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"today it "}]}')
E                +  where '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"today it "}]}' = &lt;built-in method lower of str object at 0x7fa89e56cea0&gt;()
E                +    where &lt;built-in method lower of str object at 0x7fa89e56cea0&gt; = '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"Today it "}]}'.lower
E                +      where '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"Today it "}]}' = &lt;Response [200]&gt;.text
E                +  and   '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"today it "}]}' = &lt;built-in method lower of str object at 0x7fa89e56d290&gt;()
E                +    where &lt;built-in method lower of str object at 0x7fa89e56d290&gt; = '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"Today it "}]}'.lower
E                +      where '{"id":"cmpl-d80f5d89-dcae-5d85-bc0b-a2ae56f7d8f3","created":1793566696,"model":"facebook/opt-125m","usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3},"object":"text_completion","kv_transfer_params":null,"choices":[{"index":0,"finish_reason":"stop","text":"Today it "}]}' = &lt;Response [200]&gt;.text

test/e2e/tests/test_subscription.py:3081: AssertionError</failure></testcase><testcase classname="tests.test_tenant.TestTenantLifecycle" name="test_payload_processing_deployed_with_active_tenant" time="6.930" /><testcase classname="tests.test_tenant.TestTenantLifecycle" name="test_payload_processing_and_pre_processing_resources" time="0.271"><skipped type="pytest.skip" message="default payload-processing is praxis-extproc; MaasTenantConfig.spec.payloadProcessing.resources is MaaS IPP-only">/workspace/source/test/e2e/tests/test_tenant.py:401: default payload-processing is praxis-extproc; MaasTenantConfig.spec.payloadProcessing.resources is MaaS IPP-only</skipped></testcase><testcase classname="tests.test_tenant_external_model_compat.TestLegacyIppCoexistence" name="test_no_route_on_default_gateway" time="0.000"><skipped type="pytest.skip" message="legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable">/workspace/source/test/e2e/tests/test_tenant_external_model_compat.py:375: legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable</skipped></testcase><testcase classname="tests.test_tenant_external_model_compat.TestLegacyIppCoexistence" name="test_not_served_through_default_gateway" time="0.000"><skipped type="pytest.skip" message="legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable">/workspace/source/test/e2e/tests/test_tenant_external_model_compat.py:384: legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable</skipped></testcase><testcase classname="tests.test_tenant_external_model_compat.TestLegacyIppCoexistence" name="test_no_ipp_status_writes" time="0.000"><skipped type="pytest.skip" message="legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable">/workspace/source/test/e2e/tests/test_tenant_external_model_compat.py:399: legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable</skipped></testcase><testcase classname="tests.test_tenant_external_model_compat.TestLegacyIppCoexistence" name="test_shared_provider_ownership_stays_defined" time="0.000"><skipped type="pytest.skip" message="legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable">/workspace/source/test/e2e/tests/test_tenant_external_model_compat.py:409: legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable</skipped></testcase><testcase classname="tests.test_tenant_external_model_compat.TestLegacyIppCoexistence" name="test_agc_tenant_gateway_still_serves" time="0.000"><skipped type="pytest.skip" message="legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable">/workspace/source/test/e2e/tests/test_tenant_external_model_compat.py:423: legacy IPP coexistence E2E requires the MaaS IPP image pin containing the legacy-controller exclusion guard; set TENANT_EXTERNAL_MODEL_LEGACY_IPP_E2E=true to enable</skipped></testcase><testcase classname="tests.test_tenant_namespace_discovery.TestTenantDiscoveryDormantMode" name="test_dormant_mode_ignores_labeled_namespace" time="0.425"><skipped type="pytest.skip" message="Dormant-mode test mutates controller flags; set ENABLE_TENANT_DISCOVERY_DORMANT_E2E=true">/workspace/source/test/e2e/tests/test_tenant_namespace_discovery.py:359: Dormant-mode test mutates controller flags; set ENABLE_TENANT_DISCOVERY_DORMANT_E2E=true</skipped></testcase><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_limit_name[single-rate]" time="0.001" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_limit_name[two-rates]" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_limit_name[input-order-ignored]" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_limit_name[duplicates-collapse]" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_limit_name[window-not-normalized]" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_limit_name[sorted-as-strings]" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_single_member_predicate_is_unchanged" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestExpectedShapeHelpers" name="test_multi_member_predicate_is_sorted_or" time="0.000" /><testcase classname="tests.test_trlp_rate_grouping.TestRateGrouping" name="test_same_rate_shares_limit_not_budget" time="31.761" /><testcase classname="tests.test_trlp_rate_grouping.TestRateGrouping" name="test_users_on_same_subscription_are_independent" time="19.463" /><testcase classname="tests.test_trlp_rate_grouping.TestRateGrouping" name="test_different_rates_get_separate_limits" time="36.924" /><testcase classname="tests.test_trlp_rate_grouping.TestRateGrouping" name="test_rate_edit_moves_subscription_between_limits" time="39.904" /><testcase classname="tests.test_x_api_key_auth.TestXAPIKeyAuthentication" name="test_x_api_key_authenticates" time="5.359" /><testcase classname="tests.test_x_api_key_auth.TestXAPIKeyAuthentication" name="test_authorization_bearer_still_works" time="0.134" /><testcase classname="tests.test_x_api_key_auth.TestXAPIKeyAuthentication" name="test_invalid_x_api_key_rejected" time="0.112" /><testcase classname="tests.test_x_api_key_auth.TestXAPIKeyAuthentication" name="test_x_api_key_without_prefix_rejected" time="0.107" /><testcase classname="tests.test_x_api_key_auth.TestXAPIKeyAuthentication" name="test_both_headers_no_conflict" time="0.808" /></testsuite></testsuites>