Repository navigation
Migrate infra/ Bicep to Microsoft.ApiManagement/aigateways RP and add multi-model aliases #22
Description
Problem & What Changes
infra/ in Azure-Samples/simple-foundry-hosted-agent-python-aigateway currently provisions AI Gateway on the legacy Microsoft.ApiManagement/service@2025-09-01-preview resource hierarchy (sku.name: 'AIGateway') and exposes raw catalog model names (gpt-latest, gpt-mini-latest) directly to the hosted agent without aliases resources.
We need to migrate infra/, lifecycle/postprovision scripts, and Bicep tests to the standalone Microsoft.ApiManagement/aigateways resource provider and add first-class model aliases (Microsoft.ApiManagement/aigateways/workspaces/aliases) supporting multi-model priority (failover) and weight (traffic split) routing.
While Azure/azure-rest-api-specs#47156 ([Microsoft.ApiManagement] AI Gateway 2026-11-01) defines the target ARM resource hierarchy and aliases schema, the public spec alone is not sufficient to author working Bicep against the currently deployed Azure Resource Provider. Several live RP wire-contract differences discovered while building and testing azext-aigateway must be incorporated so azd provision succeeds.
Why Azure/azure-rest-api-specs#47156 Alone Is Insufficient (Live RP vs. Spec Differences)
| Area | Azure/azure-rest-api-specs#47156 (2026-11-01 Spec) |
Live Deployed RP Behavior (Required for Working Bicep) |
|---|---|---|
| 1. ARM API Version | All resources and child types are marked @added(Versions.v2026_11_01) (with root gateway in 2026-09-01-preview). |
Live ARM only registers 2025-09-01-preview for Microsoft.ApiManagement/aigateways and its child resources. Target @2025-09-01-preview and add #disable-next-line BCP081 where Bicep type definitions are not yet published. |
| 2. Root Gateway SKU & Properties | Example AiGatewayCreate.json specifies sku: { name: "AIGateway" }. |
Live Microsoft.ApiManagement/aigateways rejects sku.name: 'AIGateway'. It requires sku: { name: 'Consumption' } (no capacity) and properties: {} (no publisherEmail / publisherName, which were APIM service fields). |
| 3. Gateway Runtime Hostname Output | AiGatewayProperties returns properties.frontend.defaultHostname (hostname without scheme) instead of properties.gatewayUrl. |
Read aiGateway.properties.frontend.defaultHostname (with fallback to replace(aiGateway.properties.?gatewayUrl ?? '', 'https://', '') if needed during transition) when constructing AZURE_AI_GATEWAY_ENDPOINT. |
4. Mandatory workspaces/default Parent |
AiGatewayWorkspaceResource (workspaces/default) is a predefined singleton child resource (GET/HEAD/PATCH). |
All workspace-scoped child resources (modelProviders, models, aliases, toolServers, telemetryExporters) must be nested under resource defaultWorkspace 'Microsoft.ApiManagement/aigateways/workspaces@2025-09-01-preview' existing = { parent: aiGateway, name: 'default' }. (apiKeys remains a direct child of aiGateway.) |
5. Provider & Model Display Name (displayName vs name) |
TypeSpec uses @encodedName("application/json", "name") on displayName for modelProviders, models, and toolServers, and omits apiFormat on models. |
The live 2025-09-01-preview wire contract uses properties.displayName (not properties.name), requires apiFormat: 'OpenAIChatCompletions' on models, and uses path-style supportedEndpoints (['/openai/v1/chat/completions', '/openai/v1/responses']). |
6. Model deployment.modelName & Creation Policies |
Spec describes deployment.modelName as "Provider-side model name" and includes inline policies in AiGatewayModelCreate.json. |
deployment.modelName must be the Foundry deployment name (last(split(model.resourceId, '/'))), not the backing catalog model name (gpt-5.6-sol). Additionally, live portal/CLI model creation (CreateCatalogModelProperties) omits inline policies on initial model PUT or applies token limits at the alias/workspace policy layer if rejected on model create. |
7. Model Aliases (workspaces/aliases) Contract |
AiGatewayAliasCreate.json includes selectionOrder: ["routing"] and allows single-model defaults (priority?: 0, weight?: 1). |
- properties.aliasName is the client-facing callable model name sent in the OpenAI request model field; two aliases on the same gateway cannot share the same aliasName (400 ValidationError).- Each backendModels[].resourceId must be the gateway child model ARM ID (/subscriptions/.../providers/Microsoft.ApiManagement/aigateways/<gw>/workspaces/default/modelProviders/<provider>/models/<model>), never the raw Microsoft.CognitiveServices/accounts/.../deployments/... ID.- priority (0..100, lower = higher priority / primary before failover) and weight (1..100, relative traffic split within the same priority tier) must both be explicitly set when 2+ backend models are configured, with no duplicate resourceIds.- Omit selectionOrder, routing, and read-only gatewayEndpoints from PUT payloads. |
| 8. Data-Plane Endpoint Prefix | Not covered in ARM control-plane TypeSpec. | Runtime OpenAI calls route through <gateway-endpoint>/default/models/openai/v1/ (e.g., /default/models/openai/v1/chat/completions) and pass properties.aliasName as the model parameter. |
File-by-File Migration Plan
1. infra/ai-gateway/main.bicep
- Root resource migration: Replace
Microsoft.ApiManagement/service@2025-09-01-preview(sku: { name: 'AIGateway', capacity: 1 },publisherEmail,publisherName) withMicrosoft.ApiManagement/aigateways@2025-09-01-previewusingsku: { name: 'Consumption' }andproperties: {}. - Child resource type migration: Update all child resource declarations to the new RP hierarchy (with
#disable-next-line BCP081annotations while targeting2025-09-01-preview):Microsoft.ApiManagement/aigateways/workspaces@2025-09-01-preview(existing,name: 'default')Microsoft.ApiManagement/aigateways/workspaces/telemetryExporters@2025-09-01-previewMicrosoft.ApiManagement/aigateways/workspaces/modelProviders@2025-09-01-previewMicrosoft.ApiManagement/aigateways/workspaces/modelProviders/models@2025-09-01-previewMicrosoft.ApiManagement/aigateways/workspaces/aliases@2025-09-01-preview(new)Microsoft.ApiManagement/aigateways/workspaces/toolServers@2025-09-01-previewMicrosoft.ApiManagement/aigateways/apiKeys@2025-09-01-preview
- Add
modelAliasesparameter and loop: Accept an array of alias definitions (armName,aliasName,description,backendModels: [{ modelArmName, priority, weight }]) and resolve each target'sresourceIdto the gateway child model resource ID (resourceId('Microsoft.ApiManagement/aigateways/workspaces/modelProviders/models', gatewayName, 'default', foundryProviderName, target.modelArmName)). - Outputs:
- Update
apimGatewayIdtoresourceId('Microsoft.ApiManagement/aigateways', gatewayName). - Update
runtimeHostnameto readaiGateway!.properties.frontend.defaultHostname(falling back toreplace(aiGateway!.properties.?gatewayUrl ?? '', 'https://', '')). - Export
modelAliasIdsanddefaultModelAliasName.
- Update
2. infra/main.bicep
- Update parameter descriptions referencing
Microsoft.ApiManagement/servicetoMicrosoft.ApiManagement/aigateways. - Add alias parameters (e.g.,
modelAliasNamedefaulting to'chat'or'gpt-latest'and optional multi-model routing configuration) and passmodelAliasesintomodule aiGateway './ai-gateway/main.bicep'. - Configure a canonical multi-model alias that routes primary traffic (
priority: 0, weight: 100) to the primary deployment (gpt-latest) and failover/secondary routing (priority: 1, weight: 100or weighted split) to the mini deployment (gpt-mini-latest), plus individual aliases if needed for direct addressing. - Ensure
AZURE_AI_GATEWAY_MODELoutputs the callablealiasNameexpected byrepo_digest_agent.py.
3. infra/scripts/configure-ai-gateway.sh & infra/scripts/configure-ai-gateway.ps1
- Invert the legacy vs. canonical resource ID check:
- Set
gateway_resource_id="/subscriptions/${subscription_id}/resourceGroups/${resource_group}/providers/Microsoft.ApiManagement/aigateways/${gateway_name}". - Treat
Microsoft.ApiManagement/serviceas the retired/legacy resource type when checking existing environments.
- Set
- Update
gateway_urlresolution to queryproperties.frontend.defaultHostname(normalizing tohttps://<hostname>/) with fallback toproperties.gatewayUrl. - Keep
verify_gateway_model_routehitting${gateway_url%/}/default/models/openai/v1/chat/completionswithAZURE_AI_GATEWAY_MODEL(now backed by the alias'saliasName).
4. infra/scripts/manage-ai-gateway-lifecycle.sh & infra/scripts/manage-ai-gateway-lifecycle.ps1
- Update
discover_live_gateway,adopt_failure_target/Use-FailureTarget,service_resource_id, andvalidate_live_ownership:- Discover
Microsoft.ApiManagement/aigatewaysinstead ofMicrosoft.ApiManagement/service. - Expect
sku.name == 'Consumption'instead ofAIGateway. - Match
microsoft.apimanagement/aigatewaysin failed deployment operation target inspection. - Handle soft-delete check gracefully (only purge
deletedservicesif applicable or skip ifaigatewaysdoes not create legacy APIMdeletedservicestombstones).
- Discover
5. tests/test-ai-gateway-model-registration.sh, tests/test-apim-lifecycle.sh, & IMPLEMENTATION_NOTES.md
- Update
tests/test-ai-gateway-model-registration.shto assert:Microsoft.ApiManagement/aigatewayswithsku.name == "Consumption".Microsoft.ApiManagement/aigateways/workspaces/modelProviders/modelswithdeployment.modelNamederived from the Foundry deployment resource ID.Microsoft.ApiManagement/aigateways/workspaces/aliaseswithaliasNameandbackendModelsreferencing gateway child model resource IDs and explicitpriorityandweight.
- Update
tests/test-apim-lifecycle.shstubs and assertions fromMicrosoft.ApiManagement/service+AIGatewaySKU toMicrosoft.ApiManagement/aigateways+ConsumptionSKU. - Update
IMPLEMENTATION_NOTES.mdto document theMicrosoft.ApiManagement/aigatewaysresource types,ConsumptionSKU, andaliasesrouting configuration.
Canonical Bicep Reference (infra/ai-gateway/main.bicep)
var aiGatewayApiVersion = '2025-09-01-preview'
var telemetryWorkspaceName = 'default'
#disable-next-line BCP081
resource aiGateway 'Microsoft.ApiManagement/aigateways@2025-09-01-preview' = if (deployApimGatewayViaBicep) {
name: gatewayName
location: gatewayLocation
tags: tags
identity: {
type: 'SystemAssigned'
}
sku: {
name: 'Consumption'
}
properties: {}
}
#disable-next-line BCP081
resource defaultWorkspace 'Microsoft.ApiManagement/aigateways/workspaces@2025-09-01-preview' existing = if (deployApimGatewayViaBicep) {
parent: aiGateway
name: telemetryWorkspaceName
}
#disable-next-line BCP081
resource foundryProvider 'Microsoft.ApiManagement/aigateways/workspaces/modelProviders@2025-09-01-preview' = if (deployFoundryProvider && deployApimGatewayViaBicep) {
parent: defaultWorkspace
name: foundryProviderName
dependsOn: [
foundryUserRoleAssignments
]
properties: {
kind: 'Foundry'
displayName: foundryProviderDisplayName
description: empty(foundryProviderDescription) ? null : foundryProviderDescription
foundry: {
endpoint: normalizedFoundryEndpoint
resourceIds: foundryResourceIds
authentication: {
kind: 'ManagedIdentity'
managedIdentity: {
resource: foundryManagedIdentityResource
}
}
}
}
}
#disable-next-line BCP081
resource foundryCatalogModels 'Microsoft.ApiManagement/aigateways/workspaces/modelProviders/models@2025-09-01-preview' = [for model in foundryModels: if (deployFoundryProvider && deployApimGatewayViaBicep) {
parent: foundryProvider
name: model.armName
properties: {
description: model.?description ?? null
displayName: model.modelName
apiFormat: model.apiFormat
supportedEndpoints: model.supportedEndpoints
deployment: {
resourceId: model.resourceId
modelName: last(split(model.resourceId, '/'))
modelVersion: model.?modelVersion ?? null
}
}
}]
#disable-next-line BCP081
resource modelAliases 'Microsoft.ApiManagement/aigateways/workspaces/aliases@2025-09-01-preview' = [for alias in modelAliasesConfig: if (deployFoundryProvider && deployApimGatewayViaBicep) {
parent: defaultWorkspace
name: alias.armName
dependsOn: [
foundryCatalogModels
]
properties: {
aliasName: alias.aliasName
description: alias.?description ?? null
backendModels: [for target in alias.backendModels: {
resourceId: resourceId(
'Microsoft.ApiManagement/aigateways/workspaces/modelProviders/models',
gatewayName,
telemetryWorkspaceName,
foundryProviderName,
target.modelArmName
)
priority: target.priority
weight: target.weight
}]
}
}]
#disable-next-line BCP081
resource runtimeApiKey 'Microsoft.ApiManagement/aigateways/apiKeys@2025-09-01-preview' = if (deployApimGatewayViaBicep) {
parent: aiGateway
name: 'default'
properties: {
displayName: 'Default hosted agent key'
}
}Example Multi-Model modelAliasesConfig Input from infra/main.bicep
modelAliasesConfig: [
{
armName: 'default-router'
aliasName: 'gpt-latest'
description: 'Primary full GPT model with automatic failover to the high-capacity mini deployment.'
backendModels: [
{
modelArmName: modelDeploymentName // 'gpt-latest' -> priority 0 primary
priority: 0
weight: 100
}
{
modelArmName: miniModelDeploymentName // 'gpt-mini-latest' -> priority 1 failover
priority: 1
weight: 100
}
]
}
]Verification Checklist for Implementation
-
az bicep build --file infra/ai-gateway/main.bicepandaz bicep build --file infra/main.bicepsucceed with zero errors. -
bash tests/test-ai-gateway-model-registration.shandbash tests/test-apim-lifecycle.shpass. -
azd provisioncreatesMicrosoft.ApiManagement/aigateways(ConsumptionSKU), registers Foundry models underworkspaces/default/modelProviders/foundry/models, creates theworkspaces/default/aliasesrouting resource(s), and passesverify_gateway_model_routeagainst/default/models/openai/v1/chat/completions.
Activity