Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,45 @@ Both features are **gated** — if a status stays `Pending` for more than a few
./scripts/register-providers.ps1 -SubscriptionId <your-sub-id>
```

### Run the all-in-one quickstart on macOS

Install PowerShell 7, Azure CLI, and Python 3, then run the same cross-platform script from a `pwsh` terminal:

```bash
brew install --cask powershell
brew install azure-cli python

az login
pwsh -File ./scripts/quickstart.ps1 \
-SubscriptionId <your-sub-id> \
-ResourceGroup <your-resource-group>
```

The script accepts the same parameters on Windows and macOS. It creates an isolated Python virtual environment and runs the demo with Azure AD authentication; no API key is required.

### Required Azure access

The simplest option is to give the deploying customer **Owner** on the target subscription for the deployment window. Owner covers resource-group creation, provider/feature registration, resource deployment, and the role assignment created by this template. Remove or reduce that access after validation.

For a least-privilege customer deployment, split the work:

1. A subscription administrator registers `Microsoft.Storage`, `Microsoft.CognitiveServices`, and the gated `Microsoft.CognitiveServices/OpenAI.ContextCacheAllowed` feature, and creates the target resource group.
2. At the target **resource-group scope**, grant the customer **Contributor** plus **Role Based Access Control Administrator**.
3. The customer runs `quickstart.ps1 -SkipPrerequisiteRegistration` against that existing resource group. This avoids subscription-level checks that a resource-group-scoped identity may not be allowed to read.

The two resource-group roles are both required by the current template:

| Role | Why it is needed |
|---|---|
| **Contributor** | Creates the Azure OpenAI account/deployment, Context Cache account/container, and ARM deployment record. |
| **Role Based Access Control Administrator** | Creates the `Cognitive Services OpenAI User` role assignment used by the keyless demo. `Contributor` explicitly cannot create role assignments. |

Provider registration requires the provider's `/register/action` permission at subscription scope; built-in **Contributor** and **Owner** include it. Preview-feature registration is also subscription-scoped and may additionally require Microsoft allow-list approval. Email **azurecontextcacherp@microsoft.com** if `OpenAI.ContextCacheAllowed` remains `Pending`.

After deployment, a person or workload calling the model with Microsoft Entra ID needs **Cognitive Services OpenAI User** on the Azure OpenAI account. The template grants it automatically to the deploying user when it creates a new account; the quickstart attempts the same grant when reusing an existing account.

RBAC alone does not guarantee deployment success. Confirm that the subscription has Azure OpenAI S0/model capacity for the selected region and is approved for the Context Cache preview before the customer session.

---

## What gets created
Expand Down
63 changes: 50 additions & 13 deletions azuredeploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,62 @@
}
},
{
"comments": "4. AOAI deployment linked to the cache container via contextCacheContainerId.",
"type": "Microsoft.CognitiveServices/accounts/deployments",
"apiVersion": "2026-03-15-preview",
"name": "[concat(variables('aoaiAccountName'), '/', variables('aoaiDeploymentName'))]",
"comments": "4. AOAI deployment linked to the cache container via contextCacheContainerId. This MUST live in a nested deployment: ARM runs RP preflight validation for every resource in a template up-front, before any resource is created. The Cognitive Services RP resolves contextCacheContainerId during preflight, so a top-level deployment resource fails with 'The context cache container could not be found or accessed' even though dependsOn is set. Nesting alone is not enough - if every nested parameter is a compile-time constant, ARM expands and preflights the nested template together with the parent. Deriving the container ID from a runtime reference() forces ARM to defer the nested deployment (and its preflight) until the container actually exists. Note reference(...,'Full').resourceId returns a provider-relative ID, so it is prefixed with resourceGroup().id to form the fully qualified ARM resource ID the RP requires.",
"type": "Microsoft.Resources/deployments",
"apiVersion": "2022-09-01",
"name": "aoaiCacheDeployment",
"dependsOn": [
"[resourceId('Microsoft.CognitiveServices/accounts', variables('aoaiAccountName'))]",
"[resourceId('Microsoft.Storage/contextCaches/contextCacheContainers', variables('cacheAccountName'), variables('cacheContainerName'))]"
],
"sku": {
"name": "[variables('skuName')]",
"capacity": "[variables('skuCapacity')]"
},
"properties": {
"model": {
"format": "[variables('modelFormat')]",
"name": "[variables('modelName')]",
"version": "[variables('modelVersion')]"
"mode": "Incremental",
"expressionEvaluationOptions": {
"scope": "Inner"
},
"parameters": {
"aoaiAccountName": { "value": "[variables('aoaiAccountName')]" },
"aoaiDeploymentName": { "value": "[variables('aoaiDeploymentName')]" },
"skuName": { "value": "[variables('skuName')]" },
"skuCapacity": { "value": "[variables('skuCapacity')]" },
"modelFormat": { "value": "[variables('modelFormat')]" },
"modelName": { "value": "[variables('modelName')]" },
"modelVersion": { "value": "[variables('modelVersion')]" },
"contextCacheContainerId": { "value": "[format('{0}/providers/{1}', resourceGroup().id, reference(resourceId('Microsoft.Storage/contextCaches/contextCacheContainers', variables('cacheAccountName'), variables('cacheContainerName')), '2026-01-01-preview', 'Full').resourceId)]" }
},
"contextCacheContainerId": "[variables('containerResourceId')]"
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"aoaiAccountName": { "type": "string" },
"aoaiDeploymentName": { "type": "string" },
"skuName": { "type": "string" },
"skuCapacity": { "type": "int" },
"modelFormat": { "type": "string" },
"modelName": { "type": "string" },
"modelVersion": { "type": "string" },
"contextCacheContainerId": { "type": "string" }
},
"resources": [
{
"type": "Microsoft.CognitiveServices/accounts/deployments",
"apiVersion": "2026-03-15-preview",
"name": "[concat(parameters('aoaiAccountName'), '/', parameters('aoaiDeploymentName'))]",
"sku": {
"name": "[parameters('skuName')]",
"capacity": "[parameters('skuCapacity')]"
},
"properties": {
"model": {
"format": "[parameters('modelFormat')]",
"name": "[parameters('modelName')]",
"version": "[parameters('modelVersion')]"
},
"contextCacheContainerId": "[parameters('contextCacheContainerId')]"
}
}
]
}
}
},
{
Expand Down
41 changes: 25 additions & 16 deletions bicep/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,33 @@ resource cacheContainer 'Microsoft.Storage/contextCaches/contextCacheContainers@
}
}

resource aoaiDeployment 'Microsoft.CognitiveServices/accounts/deployments@2026-03-15-preview' = {
parent: aoai
name: aoaiDeploymentName
// The cache-linked deployment MUST go through a nested deployment (module). ARM runs RP
// preflight validation for every resource in a template up-front, before any resource is
// created. The Cognitive Services RP resolves contextCacheContainerId during preflight, so a
// top-level deployment resource fails with 'The context cache container could not be found or
// accessed' even though dependsOn is set.
//
// Nesting alone is NOT enough: if every module parameter is a compile-time constant, ARM expands
// and preflights the nested template together with the parent. Deriving the container ID from a
// runtime reference() forces ARM to defer the nested deployment (and its preflight) until the
// container actually exists. reference(...,'Full').resourceId returns a provider-relative ID, so
// it is prefixed with the resource group ID to form the fully qualified ARM resource ID the RP
// requires.
module aoaiDeployment 'modules/aoai-cache-deployment.bicep' = {
name: 'aoaiCacheDeployment'
params: {
aoaiAccountName: aoaiAccountName
aoaiDeploymentName: aoaiDeploymentName
modelName: modelName
modelVersion: modelVersion
// Intentional raw reference(): a symbolic .id is a compile-time constant, which would let ARM
// preflight this module together with the parent and fail before the container exists.
#disable-next-line use-resource-symbol-reference
contextCacheContainerId: '${resourceGroup().id}/providers/${reference(cacheContainer.id, '2026-01-01-preview', 'Full').resourceId}'
}
dependsOn: [
aoaiNew
]
sku: {
name: 'Standard'
capacity: 100
}
properties: {
model: {
format: 'OpenAI'
name: modelName
version: modelVersion
}
contextCacheContainerId: cacheContainer.id
}
}

resource openAiUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (assignRole) {
Expand All @@ -101,7 +110,7 @@ resource openAiUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = i

output azureOpenAIAccountName string = aoaiAccountName
output azureOpenAIEndpoint string = createAoai ? aoaiNew.properties.endpoint : aoai.properties.endpoint
output aoaiDeploymentName string = aoaiDeployment.name
output aoaiDeploymentName string = aoaiDeploymentName
output contextCacheAccountName string = cacheAccount.name
output contextCacheContainerId string = cacheContainer.id
output modelName string = modelName
Expand Down
43 changes: 43 additions & 0 deletions bicep/modules/aoai-cache-deployment.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@description('Name of the Azure OpenAI (Cognitive Services) account that hosts the deployment.')
param aoaiAccountName string

@description('Name of the model deployment to create on the AOAI account.')
param aoaiDeploymentName string

@description('Model name to deploy.')
param modelName string

@description('Model version to deploy.')
param modelVersion string

@description('Resource ID of the context cache container to link the deployment to.')
param contextCacheContainerId string

@description('Deployment SKU name.')
param skuName string = 'Standard'

@description('Deployment SKU capacity.')
param skuCapacity int = 100

resource aoai 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
name: aoaiAccountName
}

resource aoaiDeployment 'Microsoft.CognitiveServices/accounts/deployments@2026-03-15-preview' = {
parent: aoai
name: aoaiDeploymentName
sku: {
name: skuName
capacity: skuCapacity
}
properties: {
model: {
format: 'OpenAI'
name: modelName
version: modelVersion
}
contextCacheContainerId: contextCacheContainerId
}
}

output deploymentName string = aoaiDeployment.name
Loading
Loading