Skip to content

Commit e47b85c

Browse files
Migrate Content Understanding from preview to GA and consolidate AI Services account
Migrate Azure AI Content Understanding from 2024-12-01-preview to GA 2025-11-01 (ADO 41641), and consolidate the standalone Content Understanding Cognitive Services account into the existing unified Azure AI Services account (now hosting both Azure OpenAI and CU). Infra - Drop avmAiServices_cu module, contentUnderstandingPrivateEndpoint, and the contentUnderstandingLocation parameter from main.bicep and main_custom.bicep; mirror the changes in main.json. - Restrict azureAiServiceLocation @Allowed to the 11-region intersection where both CU GA and gpt-5.1 GlobalStandard are available. - Add two Cognitive Services User role assignments (API and Workflow managed identities) on the unified account so CU calls don't 403. - Re-route APP_CONTENT_UNDERSTANDING_ENDPOINT to the unified account. - Drop AZURE_ENV_CU_LOCATION mapping from main.parameters.json and main.waf.parameters.json. - Remove contentUnderstandingLocation override from .github/workflows/deploy.yml. Application code - Bump api-version to 2025-11-01 and switch to the GA REST surface: :analyzeBinary for stream payloads, knowledgeSources[] for training data, and /files/{id} for figure retrieval. - Update Pydantic models for GA: add Warning, relax Page optionals (angle/spans/words/lines), and surface the new top-level DocumentContent.paragraphs field. - Add unit tests for the new Warning model and relaxed Page optionals; bump existing apiVersion fixtures. Docs - CustomizingAzdParameters.md: drop AZURE_ENV_CU_LOCATION row, rewrite AZURE_ENV_AI_SERVICE_LOCATION row, and append a usageName note for the Standard deployment type. - LocalDevelopmentSetup.md: replace stale aicu-{suffix} reference. - TroubleShootingSteps.md: update the CU 403 row for the consolidated account name and DNS zones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4be80cb commit e47b85c

13 files changed

Lines changed: 364 additions & 3588 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ jobs:
146146
--parameters \
147147
solutionName="${{ env.ENVIRONMENT_NAME }}" \
148148
enablePrivateNetworking="false" \
149-
contentUnderstandingLocation="WestUS" \
150149
deploymentType="GlobalStandard" \
151150
gptModelName="gpt-5.1" \
152151
gptModelVersion="2025-11-13" \

docs/CustomizingAzdParameters.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ By default this template will use the environment name as the prefix to prevent
1111
| -------------------------------------- | ------- | --------------------------- | ------------------------------------------------------------------------------------- |
1212
| `AZURE_ENV_NAME` | string | `cps` | Sets the environment name prefix for all Azure resources (3-20 characters). |
1313
| `AZURE_LOCATION` | string | `eastus2` | Sets the primary Azure region for resource deployment. Allowed: `australiaeast`, `centralus`, `eastasia`, `eastus2`, `japaneast`, `northeurope`, `southeastasia`, `uksouth`. |
14-
| `AZURE_ENV_CU_LOCATION` | string | `WestUS` | Sets the location for the Azure AI Content Understanding service. Allowed: `WestUS`, `SwedenCentral`, `AustraliaEast`. |
15-
| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `eastus` | Sets the location for Azure AI Services (OpenAI) deployment. |
16-
| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type. Allowed: `Standard`, `GlobalStandard`. |
14+
| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `eastus2` | Sets the location for Azure AI Services. This single account hosts both Azure OpenAI and Content Understanding. Allowed: `australiaeast`, `eastus`, `eastus2`, `japaneast`, `southcentralus`, `southeastasia`, `swedencentral`, `uksouth`, `westeurope`, `westus`, `westus3`. |
15+
| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type. Allowed: `Standard`, `GlobalStandard`.<br>**Note:** the `azd` location-picker filters regions using the `usageName` metadata on `azureAiServiceLocation` in `infra/main.bicep` (currently `OpenAI.GlobalStandard.gpt-5.1,300`). If you set this parameter to `Standard`, also edit that metadata to `OpenAI.Standard.gpt-5.1,300` so the picker shows the correct subset of regions. |
1716
| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5.1` | Specifies the GPT model name. Default: `gpt-5.1`. |
1817
| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2025-11-13` | Specifies the GPT model version. |
1918
| `AZURE_ENV_GPT_MODEL_CAPACITY` | integer | `300` | Sets the model capacity (minimum 1). Default: 300. Optimal: 500 for multi-document claim processing. |

docs/LocalDevelopmentSetup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Example resource names from deployment:
160160
- App Configuration: `appcs-{suffix}.azconfig.io`
161161
- Cosmos DB: `cosmos-{suffix}.documents.azure.com`
162162
- Storage Account: `st{suffix}.queue.core.windows.net`
163-
- Content Understanding: `aicu-{suffix}.cognitiveservices.azure.com`
163+
- Content Understanding: `aif-{suffix}.cognitiveservices.azure.com`
164164

165165
### Required Azure RBAC Permissions
166166

docs/TroubleShootingSteps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Use these as quick reference guides to unblock your deployments.
128128
| **RouteTableCannotBeAttachedForAzureBastionSubnet** | Route table attached to Azure Bastion subnet | This error occurs because Azure Bastion subnet (`AzureBastionSubnet`) has a platform restriction that prevents route tables from being attached.<br><br>**How to reproduce:**<br><ul><li>In `virtualNetwork.bicep`, add `attachRouteTable: true` to the `AzureBastionSubnet` configuration:<br>`{ name: 'AzureBastionSubnet', addressPrefixes: ['10.0.10.0/26'], attachRouteTable: true }`</li><li>Add a Route Table module to the template</li><li>Update subnet creation to attach route table conditionally:<br>`routeTableResourceId: subnet.?attachRouteTable == true ? routeTable.outputs.resourceId : null`</li><li>Deploy the template → Azure throws `RouteTableCannotBeAttachedForAzureBastionSubnet`</li></ul><br>**Resolution:**<br><ul><li>Remove the `attachRouteTable: true` flag from `AzureBastionSubnet` configuration</li><li>Ensure no route table is associated with `AzureBastionSubnet`</li><li>Route tables can only be attached to other subnets, not `AzureBastionSubnet`</li><li>For more details, refer to [Azure Bastion subnet requirements](https://learn.microsoft.com/en-us/azure/bastion/configuration-settings#subnet)</li></ul> |
129129
| **VMSizeIsNotPermittedToEnableAcceleratedNetworking** | VM size does not support accelerated networking | This error occurs when you attempt to enable accelerated networking on a VM size that does not support it. This deployment's jumpbox VM **requires** accelerated networking.<br><br>**Default VM size:** `Standard_D2s_v5` — supports accelerated networking.<br><br>**How this error happens:**<br><ul><li>You override the VM size (via `AZURE_ENV_VM_SIZE`) with a size that doesn't support accelerated networking (e.g., `Standard_A2m_v2`, A-series, or B-series VMs)</li><li>Azure rejects the deployment with `VMSizeIsNotPermittedToEnableAcceleratedNetworking`</li></ul><br>**Resolution:**<br><ul><li>Use the default `Standard_D2s_v5` (recommended)</li><li>If overriding VM size, choose one that supports accelerated networking:<br>`Standard_D2s_v4`, `Standard_D2as_v5` (AMD), `Standard_D2s_v3`</li><li>Verify VM size supports accelerated networking:<br>`az vm list-skus --location <region> --size <vm-size> --query "[?capabilities[?name=='AcceleratedNetworkingEnabled' && value=='True']]"`</li><li>Avoid A-series and B-series VMs — they do not support accelerated networking</li><li>See [VM sizes with accelerated networking](https://learn.microsoft.com/en-us/azure/virtual-network/accelerated-networking-overview)</li></ul> |
130130
| **NetworkSecurityGroupNotCompliantForAzureBastionSubnet** / **SecurityRuleParameterContainsUnsupportedValue** | NSG rules blocking required Azure Bastion ports | This error occurs when the Network Security Group (NSG) attached to `AzureBastionSubnet` explicitly denies inbound TCP ports 443 and/or 4443, which Azure Bastion requires for management and tunneling.<br><br>**How to reproduce:**<br><ul><li>Deploy the template with `enablePrivateNetworking=true` so the virtualNetwork module creates `AzureBastionSubnet` and a Network Security Group that denies ports 443 and 4443</li><li>Attempt to deploy Azure Bastion into that subnet</li><li>During validation, Bastion detects the deny rules and fails with `NetworkSecurityGroupNotCompliantForAzureBastionSubnet`</li></ul><br>**Resolution:**<br><ul><li>**Remove or modify deny rules** for ports 443 and 4443 in the NSG attached to `AzureBastionSubnet`</li><li>**Ensure required inbound rules** per [Azure Bastion NSG requirements](https://learn.microsoft.com/en-us/azure/bastion/bastion-nsg)</li><li>**Use Bicep conditions** to skip NSG attachments for `AzureBastionSubnet` if deploying Bastion</li><li>**Validate the NSG configuration** before deploying Bastion into the subnet</li></ul> |
131-
| **403 Forbidden - Content Understanding** | Azure AI Content Understanding returns 403 Forbidden in WAF (private networking) deployment | This error occurs when the **Azure AI Content Understanding** service returns a `403 Forbidden` response during document processing in a **WAF-enabled (private networking)** deployment.<br><br>**Why this happens:**<br>In WAF deployments (`enablePrivateNetworking=true`), the Content Understanding AI Services account (`aicu-<suffix>`) is configured with `publicNetworkAccess: Disabled`. All traffic must flow through the **private endpoint** (`pep-aicu-<suffix>`) and resolve via private DNS zones (`privatelink.cognitiveservices.azure.com`, `privatelink.services.ai.azure.com`, `privatelink.contentunderstanding.ai.azure.com`). If any part of this chain is misconfigured, the request either reaches the public endpoint (which is blocked) or fails to route entirely, resulting in a 403.<br><br>**Common causes:**<br><ul><li>Private DNS zones not linked to the VNet — DNS resolution falls back to the public IP, which is blocked</li><li>Private endpoint connection is not in **Approved** state</li><li>Content Understanding is deployed in a different region (`contentUnderstandingLocation`, defaults to `WestUS`) than the main deployment — the private endpoint still works cross-region, but DNS misconfiguration is more likely</li><li>Container Apps are not injected into the VNet or are on a subnet that cannot reach the private endpoint</li><li>Managed Identity used by the Container App does not have the required **Cognitive Services User** role on the Content Understanding resource</li></ul><br>**Resolution:**<br><ul><li>**Verify private endpoint status:**<br>`az network private-endpoint show --name pep-aicu-<suffix> --resource-group <rg-name> --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState.status"`<br>Expected: `Approved`</li><li>**Verify private DNS zone VNet links:**<br>`az network private-dns zone list --resource-group <rg-name> -o table`<br>Ensure `privatelink.cognitiveservices.azure.com`, `privatelink.services.ai.azure.com`, and `privatelink.contentunderstanding.ai.azure.com` all have VNet links</li><li>**Test DNS resolution from the jumpbox VM** (inside the VNet):<br>`nslookup aicu-<suffix>.cognitiveservices.azure.com`<br>Should resolve to a private IP (e.g., `10.x.x.x`), NOT a public IP</li><li>**Verify RBAC role assignments:** Ensure the Container App managed identity has **Cognitive Services User** role on the Content Understanding resource:<br>`az role assignment list --scope /subscriptions/<sub-id>/resourceGroups/<rg-name>/providers/Microsoft.CognitiveServices/accounts/aicu-<suffix> --query "[?roleDefinitionName=='Cognitive Services User']" -o table`</li><li>**Check Container App VNet integration:** Confirm the Container App Environment is deployed into the VNet and can reach the backend subnet where the private endpoint resides</li><li>**Redeploy if needed:**<br>`azd up`</li></ul><br>**Reference:**<br><ul><li>[Configure private endpoints for Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-virtual-networks)</li><li>[Azure Private DNS zones](https://learn.microsoft.com/en-us/azure/dns/private-dns-overview)</li></ul> |
131+
| **403 Forbidden - Content Understanding** | Azure AI Content Understanding returns 403 Forbidden in WAF (private networking) deployment | This error occurs when the **Azure AI Content Understanding** API on the unified AI Services account (`aif-<suffix>`) returns a `403 Forbidden` response during document processing in a **WAF-enabled (private networking)** deployment.<br><br>**Why this happens:**<br>As of the CU GA migration, Content Understanding shares the same Azure AI Services account as Azure OpenAI (`aif-<suffix>`). In WAF deployments (`enablePrivateNetworking=true`), that account is configured with `publicNetworkAccess: Disabled`. All traffic must flow through the unified private endpoint (`pep-aiservices-<suffix>`) and resolve via four private DNS zones: `privatelink.cognitiveservices.azure.com`, `privatelink.openai.azure.com`, `privatelink.services.ai.azure.com`, and `privatelink.contentunderstanding.ai.azure.com`. If any link in this chain is misconfigured, the request either reaches the public endpoint (blocked) or fails to route, resulting in a 403.<br><br>**Common causes:**<br><ul><li>Private DNS zones not linked to the VNet — DNS resolution falls back to the public IP, which is blocked</li><li>Private endpoint connection is not in **Approved** state</li><li>Container Apps are not injected into the VNet or are on a subnet that cannot reach the private endpoint</li><li>Managed Identity used by the Container App does not have the required **Cognitive Services User** role on the unified AI Services account</li><li>Reusing an existing AI Foundry project (`existingFoundryProjectResourceId`): the repo no longer creates a CU-specific PE; the existing account must have its own private endpoint covering the four DNS zones above</li></ul><br>**Resolution:**<br><ul><li>**Verify private endpoint status:**<br>`az network private-endpoint show --name pep-aiservices-<suffix> --resource-group <rg-name> --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState.status"`<br>Expected: `Approved`</li><li>**Verify private DNS zone VNet links:**<br>`az network private-dns zone list --resource-group <rg-name> -o table`<br>Ensure `privatelink.cognitiveservices.azure.com`, `privatelink.openai.azure.com`, `privatelink.services.ai.azure.com`, and `privatelink.contentunderstanding.ai.azure.com` all have VNet links</li><li>**Test DNS resolution from the jumpbox VM** (inside the VNet):<br>`nslookup aif-<suffix>.cognitiveservices.azure.com`<br>Should resolve to a private IP (e.g., `10.x.x.x`), NOT a public IP</li><li>**Verify RBAC role assignments:** ensure the Container App managed identity has **Cognitive Services User** role on the unified account:<br>`az role assignment list --scope /subscriptions/<sub-id>/resourceGroups/<rg-name>/providers/Microsoft.CognitiveServices/accounts/aif-<suffix> --query "[?roleDefinitionName=='Cognitive Services User']" -o table`</li><li>**Check Container App VNet integration:** confirm the Container App Environment is deployed into the VNet and can reach the backend subnet where the private endpoint resides</li><li>**Redeploy if needed:**<br>`azd up`</li></ul><br>**Reference:**<br><ul><li>[Configure private endpoints for Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-virtual-networks)</li><li>[Azure Private DNS zones](https://learn.microsoft.com/en-us/azure/dns/private-dns-overview)</li></ul> |
132132

133133
---------------------------------
134134

infra/main.bicep

Lines changed: 20 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,20 @@ param solutionName string = 'cps'
2525
param location string
2626

2727
@minLength(1)
28-
@description('Optional. Location for the Azure AI Content Understanding service deployment.')
29-
@allowed(['WestUS', 'SwedenCentral', 'AustraliaEast'])
30-
@metadata({
31-
azd: {
32-
type: 'location'
33-
}
34-
})
35-
param contentUnderstandingLocation string = 'WestUS'
36-
3728
@allowed([
3829
'australiaeast'
39-
'centralus'
40-
'eastasia'
30+
'eastus'
4131
'eastus2'
4232
'japaneast'
43-
'northeurope'
33+
'southcentralus'
4434
'southeastasia'
35+
'swedencentral'
4536
'uksouth'
37+
'westeurope'
38+
'westus'
39+
'westus3'
4640
])
47-
@description('Required. Location for the Azure AI Services deployment.')
41+
@description('Required. Location for the Azure AI Services deployment. Must support both Azure OpenAI gpt-5.1 (GlobalStandard) and Azure AI Content Understanding GA. If the deploymentType param is set to Standard, override the metadata.azd.usageName below to reference OpenAI.Standard.gpt-5.1 instead.')
4842
@metadata({
4943
azd: {
5044
type: 'location'
@@ -747,6 +741,16 @@ module avmAiServices 'modules/account/aifoundry.bicep' = {
747741
roleDefinitionIdOrName: 'Azure AI Developer'
748742
principalType: 'ServicePrincipal'
749743
}
744+
{
745+
principalId: avmContainerApp.outputs.systemAssignedMIPrincipalId!
746+
roleDefinitionIdOrName: 'Cognitive Services User'
747+
principalType: 'ServicePrincipal'
748+
}
749+
{
750+
principalId: avmContainerApp_Workflow.outputs.systemAssignedMIPrincipalId!
751+
roleDefinitionIdOrName: 'Cognitive Services User'
752+
principalType: 'ServicePrincipal'
753+
}
750754
]
751755
networkAcls: {
752756
bypass: 'AzureServices'
@@ -816,84 +820,6 @@ module cognitiveServicePrivateEndpoint 'br/public:avm/res/network/private-endpoi
816820
}
817821
}
818822

819-
module avmAiServices_cu 'br/public:avm/res/cognitive-services/account:0.14.2' = {
820-
name: take('avm.res.cognitive-services.account.content-understanding.${solutionSuffix}', 64)
821-
822-
params: {
823-
name: 'aicu-${solutionSuffix}'
824-
location: contentUnderstandingLocation
825-
sku: 'S0'
826-
managedIdentities: {
827-
systemAssigned: false
828-
userAssignedResourceIds: [
829-
avmManagedIdentity.outputs.resourceId // Use the managed identity created above
830-
]
831-
}
832-
kind: 'AIServices'
833-
tags: {
834-
app: solutionSuffix
835-
location: location
836-
}
837-
customSubDomainName: 'aicu-${solutionSuffix}'
838-
disableLocalAuth: true
839-
enableTelemetry: enableTelemetry
840-
networkAcls: {
841-
bypass: 'AzureServices'
842-
defaultAction: 'Allow' // Always allow for AI Services
843-
}
844-
roleAssignments: [
845-
{
846-
principalId: avmContainerApp.outputs.systemAssignedMIPrincipalId!
847-
roleDefinitionIdOrName: 'a97b65f3-24c7-4388-baec-2e87135dc908'
848-
principalType: 'ServicePrincipal'
849-
}
850-
{
851-
principalId: avmContainerApp_Workflow.outputs.systemAssignedMIPrincipalId!
852-
roleDefinitionIdOrName: 'a97b65f3-24c7-4388-baec-2e87135dc908'
853-
principalType: 'ServicePrincipal'
854-
}
855-
]
856-
857-
publicNetworkAccess: (enablePrivateNetworking) ? 'Disabled' : 'Enabled'
858-
}
859-
}
860-
861-
module contentUnderstandingPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.12.0' = if (enablePrivateNetworking) {
862-
name: take('avm.res.network.private-endpoint.aicu-${solutionSuffix}', 64)
863-
params: {
864-
name: 'pep-aicu-${solutionSuffix}'
865-
location: location
866-
tags: tags
867-
customNetworkInterfaceName: 'nic-aicu-${solutionSuffix}'
868-
privateLinkServiceConnections: [
869-
{
870-
name: 'pep-aicu-${solutionSuffix}-cognitiveservices-connection'
871-
properties: {
872-
privateLinkServiceId: avmAiServices_cu.outputs.resourceId
873-
groupIds: ['account']
874-
}
875-
}
876-
]
877-
privateDnsZoneGroup: {
878-
privateDnsZoneGroupConfigs: [
879-
{
880-
name: 'aicu-dns-zone-cognitiveservices'
881-
privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cognitiveServices]!.outputs.resourceId
882-
}
883-
{
884-
name: 'ai-services-dns-zone-aiservices'
885-
privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.aiServices]!.outputs.resourceId
886-
}
887-
{
888-
name: 'aicu-dns-zone-contentunderstanding'
889-
privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.contentUnderstanding]!.outputs.resourceId
890-
}
891-
]
892-
}
893-
subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId
894-
}
895-
}
896-
897823
// ========== Container App Environment ========== //
898824
module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
899825
name: take('avm.res.app.managed-environment.${solutionSuffix}', 64)
@@ -1408,7 +1334,7 @@ module avmAppConfig 'br/public:avm/res/app-configuration/configuration-store:0.9
14081334
}
14091335
{
14101336
name: 'APP_CONTENT_UNDERSTANDING_ENDPOINT'
1411-
value: avmAiServices_cu.outputs.endpoint //TODO: replace with actual endpoint
1337+
value: avmAiServices.outputs.endpoint
14121338
}
14131339
{
14141340
name: 'APP_COSMOS_CONTAINER_PROCESS'
@@ -1683,7 +1609,6 @@ module avmContainerApp_update 'br/public:avm/res/app/container-app:0.22.1' = {
16831609
}
16841610
dependsOn: [
16851611
cognitiveServicePrivateEndpoint
1686-
contentUnderstandingPrivateEndpoint
16871612
]
16881613
}
16891614

@@ -1922,8 +1847,8 @@ output CONTAINER_REGISTRY_NAME string = avmContainerRegistry.outputs.name
19221847
@description('The login server of the Azure Container Registry.')
19231848
output CONTAINER_REGISTRY_LOGIN_SERVER string = avmContainerRegistry.outputs.loginServer
19241849

1925-
@description('The name of the Content Understanding AI Services account.')
1926-
output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name
1850+
@description('The name of the AI Services account that hosts both Azure OpenAI and Content Understanding GA.')
1851+
output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices.outputs.name
19271852

19281853
@description('The resource group the resources were deployed into.')
19291854
output AZURE_RESOURCE_GROUP string = resourceGroup().name

0 commit comments

Comments
 (0)