External Model Setup (Tech Preview)
Tech Preview
ExternalModel support is a tech preview feature. APIs and behavior may change in future releases.
This guide walks through deploying an external AI/ML model (e.g., OpenAI, Anthropic) through the MaaS gateway. External models are hosted outside the cluster — MaaS handles authentication, rate limiting, and API key management while routing inference requests to the external provider.
Multi-Tenant Limitation
External models are only supported for the default tenant
In multi-tenant deployments, external models work for the default tenant only. Non-default tenant IPP (Inference Payload Processor) instances have the ExternalModel controller disabled to prevent HTTPRoute conflicts between tenants.
External model support for non-default tenants is planned for a future release.
Prerequisites
- MaaS platform deployed per the Installation Guide
kubectl/ocaccess as cluster-admin- An API key from the external provider (e.g., OpenAI)
Architecture
An external model deployment involves the following resources:
| Resource | Created by | Purpose |
|---|---|---|
| ExternalModel | User | Defines the external provider, endpoint, and credential reference |
| MaaSModelRef | User | Registers the model in the MaaS catalog |
| MaaSAuthPolicy | User | Defines which groups can access the model |
| MaaSSubscription | User | Defines token rate limits for the model |
| Service (ExternalName) | ExternalModel reconciler | Maps an in-cluster DNS name to the external FQDN |
| ServiceEntry | ExternalModel reconciler | Registers the external host in the Istio mesh |
| DestinationRule | ExternalModel reconciler | Configures TLS origination for the external endpoint |
| HTTPRoute | ExternalModel reconciler | Routes gateway traffic to the external provider |
| Kuadrant AuthPolicy | MaaSAuthPolicy controller | Per-route auth enforcement via Authorino |
| TokenRateLimitPolicy | MaaSSubscription controller | Per-route rate limiting via Limitador |
The RHOAI 3.5 Inference Payload Processor (IPP) component handled provider API key injection, model routing, and the formats registered by its api-translation plugin. Its default profile enabled request processing but left response processing empty. That legacy behavior is not proof that the 3.6 Praxis ExternalModel runtime preserves every provider direction or stream mode; see the 3.6 upgrade guide.
Step 1: Deploy Inference Payload Processor (IPP)
IPP is required for external models — it injects the provider API key and translates between OpenAI-compatible format and the provider's native API.
MaaS deploys the payload-processing component from the ai-gateway-payload-processing repository. For detailed configuration and usage, see that project's documentation.
IPP response translation (opt-in)
The exact 3.5 product-default profile ran api-translation on the request path; response-side api-translation was off. Thus pure-default already had request-side translation. Baseline #2 (default plus response api-translation) additionally enabled response processing.
The legacy IPP api-translation plugin was a multi-provider catch-all documented for OpenAI, Anthropic, Azure OpenAI, Vertex AI, and Bedrock Mantle. Current Praxis qualification is narrower. spec.apiTranslation.enabled is one tenant permission for the shared Anthropic Messages client → OpenAI-compatible backend flow and the ExternalModel OpenAI Chat Completions client → selected Anthropic Messages provider flow. For ExternalModels, intelligent_route selects the provider first; its trusted profile controls request and response translation. OpenAI openai-chat candidates remain pass-through. The currently qualified ExternalModel profiles are OpenAI openai-chat and Anthropic messages; Azure OpenAI, Vertex AI, Bedrock, custom provider types, and other formats are not claimed as supported. Do not infer their support from the legacy IPP ConfigMap.
The current ExternalModel ExtProc configuration buffers each selected-provider request body up to 1 MiB for validation and target-model rewriting. Larger requests are rejected before reaching the provider, including for OpenAI-compatible candidates.
GATEWAY_NAMESPACE="${GATEWAY_NAMESPACE:-openshift-ingress}"
# 1. Edit the live ConfigMap — under profiles[0].plugins.response add:
# - pluginRef: api-translation
kubectl edit configmap payload-processing-plugins -n "${GATEWAY_NAMESPACE}"
# 2. Reload IPP
kubectl rollout restart deployment/payload-processing -n "${GATEWAY_NAMESPACE}"
To reset the ConfigMap to product defaults once, remove the opt-out annotation (or set opendatahub.io/managed=true for continuous reconciler management), wait for reconcile, then optionally set opendatahub.io/managed=false again after editing:
# One-shot reset to defaults (controller re-applies, then stamps managed=false again)
kubectl annotate configmap payload-processing-plugins -n "${GATEWAY_NAMESPACE}" opendatahub.io/managed-
# Or keep the reconciler owning the ConfigMap continuously:
kubectl annotate configmap payload-processing-plugins -n "${GATEWAY_NAMESPACE}" opendatahub.io/managed=true --overwrite
Note
If MaaS was deployed via the MaasTenantConfig CR (standard RHOAI path), IPP is already deployed as a subcomponent. Verify with:
If the pod is already running, skip to Step 2.
PROJECT_DIR=$(git rev-parse --show-toplevel)
# RBAC
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/rbac/serviceaccount.yaml
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/rbac/clusterrole.yaml
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/rbac/clusterrolebinding.yaml
# ConfigMap, Service, DestinationRule, EnvoyFilter
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/manager/plugins-configmap.yaml
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/manager/service.yaml
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/manager/destination-rule.yaml
kubectl apply -f ${PROJECT_DIR}/deployment/base/payload-processing/manager/envoy-filter.yaml
# Deployment (substitute the image placeholder)
PAYLOAD_PROCESSING_IMAGE="${PAYLOAD_PROCESSING_IMAGE:-$(grep '^payload-processing-image=' "${PROJECT_DIR}/deployment/base/maas-controller/default/params.env" | cut -d= -f2-)}"
cat ${PROJECT_DIR}/deployment/base/payload-processing/manager/deployment.yaml | \
sed "s|image: payload-processing|image: ${PAYLOAD_PROCESSING_IMAGE}|" | \
kubectl apply -f -
# Verify
kubectl get pods -n openshift-ingress -l app=payload-processing
The pod should be 1/1 Running.
Step 2: Create the Model Namespace
External models deploy to a model namespace (e.g., llm). If it doesn't exist:
Step 3: Create the Provider API Key Secret
Store the external provider's API key in a Kubernetes Secret. The Secret must:
- Be in the same namespace as the ExternalModel
- Use the data key
api-key - Have the label
inference.llm-d.ai/ipp-managed=trueso IPP can read it
TMP_KEY_FILE="$(mktemp)"
chmod 600 "${TMP_KEY_FILE}"
cat > "${TMP_KEY_FILE}" <<< "YOUR_OPENAI_API_KEY"
kubectl create secret generic openai-api-key -n llm \
--from-file=api-key="${TMP_KEY_FILE}"
rm -f "${TMP_KEY_FILE}"
kubectl label secret openai-api-key -n llm inference.llm-d.ai/ipp-managed=true
Step 4: Create the ExternalModel and MaaSModelRef
The ExternalModel defines the provider connection. The MaaSModelRef registers it in the MaaS catalog.
kubectl apply -f - <<'EOF'
apiVersion: maas.opendatahub.io/v1alpha1
kind: ExternalModel
metadata:
name: gpt-4o
namespace: llm
spec:
provider: openai
endpoint: api.openai.com
targetModel: gpt-4o
credentialRef:
name: openai-api-key
---
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSModelRef
metadata:
name: gpt-4o
namespace: llm
spec:
modelRef:
kind: ExternalModel
name: gpt-4o
EOF
Verify the model is ready:
Expected output:
NAME PHASE ENDPOINT HTTPROUTE GATEWAY
gpt-4o Ready https://maas.<cluster-domain>/llm/gpt-4o maas-gpt-4o maas-default-gateway
Annotations
ExternalModel supports optional annotations to control networking behavior:
| Annotation | Required | Default | Description |
|---|---|---|---|
maas.opendatahub.io/tls |
No | true |
Controls TLS origination to the external endpoint. Set to false for non-TLS endpoints. |
maas.opendatahub.io/port |
No | 443 |
Overrides the default port for the external endpoint. Valid range: 1–65535; values outside this range are rejected during reconciliation. |
By default, the ExternalModel reconciler enables TLS origination — the Istio sidecar performs the TLS handshake with the external provider. This is the correct setting for public providers like OpenAI and Anthropic.
For internal endpoints that do not use TLS (e.g., a self-hosted vLLM instance), disable TLS origination and set the appropriate port:
Cleartext credential traffic
Disabling TLS while credentialRef is set means the provider API key is sent in cleartext between the gateway and the endpoint. Only use non-TLS mode on a trusted, isolated network.
apiVersion: maas.opendatahub.io/v1alpha1
kind: ExternalModel
metadata:
name: internal-vllm
namespace: llm
annotations:
maas.opendatahub.io/tls: "false"
maas.opendatahub.io/port: "8000"
spec:
provider: openai
endpoint: vllm.internal.example.com
targetModel: my-model
credentialRef:
name: vllm-api-key
When TLS is disabled, the reconciler creates the ServiceEntry with HTTP protocol instead of HTTPS and does not create a DestinationRule. Any existing controller-managed DestinationRule is deleted; DestinationRules annotated with opendatahub.io/managed: "false" are preserved.
For full annotation details, see the ExternalModel CRD Reference.
Step 5: Configure Access and Rate Limits
Create a MaaSAuthPolicy (who can access) and MaaSSubscription (rate limits) in the models-as-a-service namespace:
kubectl apply -f - <<'EOF'
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSAuthPolicy
metadata:
name: gpt-4o-access
namespace: models-as-a-service
spec:
modelRefs:
- name: gpt-4o
namespace: llm
subjects:
groups:
- name: "system:authenticated"
---
apiVersion: maas.opendatahub.io/v1alpha1
kind: MaaSSubscription
metadata:
name: gpt-4o-subscription
namespace: models-as-a-service
spec:
owner:
groups:
- name: "system:authenticated"
modelRefs:
- name: gpt-4o
namespace: llm
tokenRateLimits:
- limit: 100000
window: "1h"
EOF
Step 6: Validate
Mint an API Key
GW_HOST=$(kubectl get gateway maas-default-gateway -n openshift-ingress \
-o jsonpath='{.spec.listeners[0].hostname}')
TOKEN=$(oc whoami -t)
KEY=$(curl -sS -X POST "https://${GW_HOST}/maas-api/v1/api-keys" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"external-model-key","subscription":"gpt-4o-subscription"}' | jq -r '.key')
echo "MaaS API key: ${KEY:0:20}..."
Run Inference
curl -sS "https://${GW_HOST}/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${KEY}" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"say hello"}]}'
Legacy path-based endpoint
You can also use the path-based URL: https://${GW_HOST}/llm/gpt-4o/v1/chat/completions. Both routing modes are supported. See Inference - Path-Based Routing for details.
Verify Auth Enforcement
# Bogus key - expect 403
curl -sS -w "HTTP: %{http_code}\n" "https://${GW_HOST}/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-oai-FAKE-KEY" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'
# No auth - expect 401
curl -sS -w "HTTP: %{http_code}\n" "https://${GW_HOST}/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'
Verify Model Listing
The model gpt-4o should appear in the list.
TLS certificate errors
If curl returns curl: (60) SSL certificate problem, see Troubleshooting - TLS Certificate Validation.
Supported Providers
The table below records the RHOAI 3.5 IPP guide's documented provider behavior. It is historical coverage, not a support matrix for the 3.6 Praxis ExternalModel dataplane. The Phase 1 Kind qualification proves the separate shared Anthropic Messages client → OpenAI-compatible backend path. It does not prove OpenAI-client → Anthropic-provider parity or Praxis support for Azure OpenAI, Vertex AI, or Bedrock. See the 3.6 upgrade guide for the migration gate and currently supported ExternalModel formats.
| Provider | spec.provider |
Translation | Auth Header |
|---|---|---|---|
| OpenAI | openai |
Pass-through | Authorization: Bearer |
| Anthropic | anthropic |
OpenAI ↔ Messages API | x-api-key |
| Azure OpenAI | azure-openai |
Path rewrite + field stripping | api-key |
| Vertex AI | vertex-openai |
Path rewrite + field stripping | Authorization: Bearer (OAuth) |
| AWS Bedrock | bedrock-openai |
Pass-through (Mantle) | Authorization: Bearer |
OpenAI
Pass-through — no body translation needed. Auth uses Authorization: Bearer.
- Set
endpoint: api.openai.com - Models:
gpt-4o,gpt-4o-mini,gpt-4.1,gpt-4.1-mini,o1,o3-mini, etc.
See the OpenAI provider guide.
Anthropic
Translates OpenAI Chat Completions to Anthropic Messages API format. Auth uses x-api-key header (not Authorization: Bearer).
- System messages extracted to top-level
systemfield tools[]converted to Anthropic format withinput_schematool_choicemapped:auto→{"type":"auto"},required→{"type":"any"}streamfield forwarded for streaming supportanthropic-version: 2023-06-01header added automatically- Response
stop_reasonmapped back to OpenAIfinish_reason - Unsupported parameters silently dropped:
frequency_penalty,presence_penalty,logprobs,n,response_format,seed - Set
endpoint: api.anthropic.com - Models:
claude-sonnet-4-20250514,claude-haiku-4-5-20251001,claude-opus-4-20250514
See the Anthropic provider guide.
Azure OpenAI
Uses Azure's OpenAI-compatible endpoint with a different auth header and path. Auth uses api-key header (not Authorization: Bearer).
- Path rewritten to
/openai/v1/chat/completions - Response strips Azure-specific fields:
content_filter_results,prompt_filter_results targetModelmust match the deployment name in your Azure OpenAI resource- Set
endpoint: <resource>.openai.azure.com(e.g.,my-deployment.openai.azure.com)
See the Azure OpenAI provider guide.
AWS Bedrock (OpenAI-compatible)
Routes to AWS Bedrock's OpenAI-compatible Mantle endpoint. Pass-through — no body translation. Auth uses Authorization: Bearer with a Bedrock API Key (starts with ABSK, not AWS access keys).
Warning
Use bedrock-mantle.<region>.api.aws, not bedrock-runtime.<region>.amazonaws.com. The IPP translator uses /v1/chat/completions which is only available on the Bedrock Mantle endpoint. Using bedrock-runtime will result in 404 errors.
- Set
endpoint: bedrock-mantle.<region>.api.aws(e.g.,bedrock-mantle.us-east-2.api.aws) - Models vary by region. List available models:
curl -s "https://bedrock-mantle.<region>.api.aws/v1/models" -H "Authorization: Bearer <KEY>"
See the Bedrock provider guide.
Vertex AI (OpenAI-compatible)
Routes to Google Vertex AI's OpenAI-compatible endpoint. Auth uses OAuth2 Bearer tokens.
Warning
Unlike other providers, vertex-openai requires IPP plugin configuration with your GCP project, location, and endpoint. Without this configuration, the translator will not be registered and requests will fail.
- Requires plugin-level config:
project,location,endpoint(set in Helm values or ConfigMap) - OAuth2 tokens expire every hour — the Secret must be refreshed with
gcloud auth print-access-token targetModelmust usepublisher/modelformat (e.g.,google/gemini-2.5-flash, notgemini-2.5-flash)- Response strips
usage.extra_properties - Set
endpoint: <region>-aiplatform.googleapis.com(e.g.,us-central1-aiplatform.googleapis.com) - Models:
google/gemini-2.5-flash,google/gemini-2.5-pro,google/gemini-2.0-flash
See the Vertex AI provider guide.
For detailed per-provider configuration, examples, and troubleshooting, see the IPP provider guides.
Cleanup
To remove an external model and all its managed resources:
# Delete the ExternalModel — OwnerReferences clean up Service, ServiceEntry, DR, HTTPRoute
kubectl delete externalmodel gpt-4o -n llm
# Delete the MaaSModelRef
kubectl delete maasmodelref gpt-4o -n llm
# Delete auth and subscription
kubectl delete maasauthpolicy gpt-4o-access -n models-as-a-service
kubectl delete maassubscription gpt-4o-subscription -n models-as-a-service
# Delete the provider secret
kubectl delete secret openai-api-key -n llm