Skip to content

Migrate infra/ Bicep to Microsoft.ApiManagement/aigateways RP and add multi-model aliases #22

Closed

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) with Microsoft.ApiManagement/aigateways@2025-09-01-preview using sku: { name: 'Consumption' } and properties: {}.
  • Child resource type migration: Update all child resource declarations to the new RP hierarchy (with #disable-next-line BCP081 annotations while targeting 2025-09-01-preview):
    • Microsoft.ApiManagement/aigateways/workspaces@2025-09-01-preview (existing, name: 'default')
    • Microsoft.ApiManagement/aigateways/workspaces/telemetryExporters@2025-09-01-preview
    • Microsoft.ApiManagement/aigateways/workspaces/modelProviders@2025-09-01-preview
    • Microsoft.ApiManagement/aigateways/workspaces/modelProviders/models@2025-09-01-preview
    • Microsoft.ApiManagement/aigateways/workspaces/aliases@2025-09-01-preview (new)
    • Microsoft.ApiManagement/aigateways/workspaces/toolServers@2025-09-01-preview
    • Microsoft.ApiManagement/aigateways/apiKeys@2025-09-01-preview
  • Add modelAliases parameter and loop: Accept an array of alias definitions (armName, aliasName, description, backendModels: [{ modelArmName, priority, weight }]) and resolve each target's resourceId to the gateway child model resource ID (resourceId('Microsoft.ApiManagement/aigateways/workspaces/modelProviders/models', gatewayName, 'default', foundryProviderName, target.modelArmName)).
  • Outputs:
    • Update apimGatewayId to resourceId('Microsoft.ApiManagement/aigateways', gatewayName).
    • Update runtimeHostname to read aiGateway!.properties.frontend.defaultHostname (falling back to replace(aiGateway!.properties.?gatewayUrl ?? '', 'https://', '')).
    • Export modelAliasIds and defaultModelAliasName.

2. infra/main.bicep

  • Update parameter descriptions referencing Microsoft.ApiManagement/service to Microsoft.ApiManagement/aigateways.
  • Add alias parameters (e.g., modelAliasName defaulting to 'chat' or 'gpt-latest' and optional multi-model routing configuration) and pass modelAliases into module 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: 100 or weighted split) to the mini deployment (gpt-mini-latest), plus individual aliases if needed for direct addressing.
  • Ensure AZURE_AI_GATEWAY_MODEL outputs the callable aliasName expected by repo_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/service as the retired/legacy resource type when checking existing environments.
  • Update gateway_url resolution to query properties.frontend.defaultHostname (normalizing to https://<hostname>/) with fallback to properties.gatewayUrl.
  • Keep verify_gateway_model_route hitting ${gateway_url%/}/default/models/openai/v1/chat/completions with AZURE_AI_GATEWAY_MODEL (now backed by the alias's aliasName).

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, and validate_live_ownership:
    • Discover Microsoft.ApiManagement/aigateways instead of Microsoft.ApiManagement/service.
    • Expect sku.name == 'Consumption' instead of AIGateway.
    • Match microsoft.apimanagement/aigateways in failed deployment operation target inspection.
    • Handle soft-delete check gracefully (only purge deletedservices if applicable or skip if aigateways does not create legacy APIM deletedservices tombstones).

5. tests/test-ai-gateway-model-registration.sh, tests/test-apim-lifecycle.sh, & IMPLEMENTATION_NOTES.md

  • Update tests/test-ai-gateway-model-registration.sh to assert:
    • Microsoft.ApiManagement/aigateways with sku.name == "Consumption".
    • Microsoft.ApiManagement/aigateways/workspaces/modelProviders/models with deployment.modelName derived from the Foundry deployment resource ID.
    • Microsoft.ApiManagement/aigateways/workspaces/aliases with aliasName and backendModels referencing gateway child model resource IDs and explicit priority and weight.
  • Update tests/test-apim-lifecycle.sh stubs and assertions from Microsoft.ApiManagement/service + AIGateway SKU to Microsoft.ApiManagement/aigateways + Consumption SKU.
  • Update IMPLEMENTATION_NOTES.md to document the Microsoft.ApiManagement/aigateways resource types, Consumption SKU, and aliases routing 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.bicep and az bicep build --file infra/main.bicep succeed with zero errors.
  • bash tests/test-ai-gateway-model-registration.sh and bash tests/test-apim-lifecycle.sh pass.
  • azd provision creates Microsoft.ApiManagement/aigateways (Consumption SKU), registers Foundry models under workspaces/default/modelProviders/foundry/models, creates the workspaces/default/aliases routing resource(s), and passes verify_gateway_model_route against /default/models/openai/v1/chat/completions.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions