Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ If the Anthropic SDK ever accepts a callable for `auth_token`, this shim becomes
- One **Claude deployment per requested family** (`GlobalStandard`, with the required `modelProviderData` block) — set `CLAUDE_HAIKU_MODEL` / `CLAUDE_SONNET_MODEL` / `CLAUDE_OPUS_MODEL` to control which families. Sonnet/Opus deployments chain on the prior to avoid Foundry's per-account 409s on concurrent create.
- *Optional* RBAC: *Foundry User* + *Foundry Project Manager* on the deploying principal (set `ASSIGN_RBAC=true`). *(These roles were previously called `Azure AI User` / `Azure AI Project Manager`; Azure renamed them — the underlying role GUIDs are unchanged.)*
- **Heads up:** without this (or a manual post-deploy grant), the Python SDK and `claude` CLI will return `401 PermissionDenied` even though `azd up` succeeded. See [Granting data-plane roles after `azd up`](#granting-data-plane-roles-after-azd-up).
- When `ASSIGN_RBAC=true`, the model deployments are ordered to run *after* both role assignments. The role-assignment PUTs return fast (~5 s) but Foundry data-plane RBAC takes a few minutes to propagate; the slow model-deployment LRO (30 s–20 min) absorbs that propagation time so the first call after `azd up` succeeds without retries.

</details>

Expand Down Expand Up @@ -477,7 +478,7 @@ claude/
| Preflight: `Marketplace offer ... not found` | `CLAUDE_MODEL_NAME` is misspelled, the model isn't in the Anthropic-on-Foundry catalog yet, or Anthropic changed the plan-name convention. |
| Preflight: `Quota insufficient` (exit 6) | Requested `CLAUDE_*_CAPACITY` plus existing usage exceeds the per-region quota limit. Lower the requested capacity, free up quota by deleting unused deployments, or [purge soft-deleted accounts](#free-quota-held-by-soft-deleted-accounts) that may still be holding TPM. |
| Quota looks full but you have no live deployments (`az cognitiveservices usage list` shows `currentValue > 0`, deployment still fails with `715-123420` / `InsufficientQuota`) | **Soft-deleted Cognitive Services accounts still reserve quota for 48 h.** A previous `azd down` (or any RG / account delete) puts the AIServices account in a recoverable state that keeps holding TPM. **Fix:** list and purge them: `az cognitiveservices account list-deleted -o table` then `az cognitiveservices account purge --name <name> --location <region> --resource-group <rg>` for each. See [Free quota held by soft-deleted accounts](#free-quota-held-by-soft-deleted-accounts). |
| `401 PermissionDenied: Principal does not have access to API/Operation` intermittently &mdash; same code passes seconds later | Data-plane RBAC propagation lag on a freshly-granted role (`Cognitive Services User` / `Foundry User` / `Azure AI Developer`). The grant can take a few minutes to land on the Foundry data plane even after `az role assignment create` returns. Wait a minute and retry; if it still fails consistently, verify the role with `az role assignment list --assignee <oid> --scope <foundry-account-id> -o table`. |
| `401 PermissionDenied: Principal does not have access to API/Operation` intermittently &mdash; same code passes seconds later | Data-plane RBAC propagation lag on a freshly-granted role (`Cognitive Services User` / `Foundry User` / `Azure AI Developer`). The grant can take a few minutes to land on the Foundry data plane even after `az role assignment create` returns. When `ASSIGN_RBAC=true`, this kit serializes the model deployments after the role assignments so the deployment LRO absorbs the propagation wait &mdash; the first call after `azd up` should just work. If you granted the role manually *after* `azd up`, wait a minute and retry; verify the assignment with `az role assignment list --assignee <oid> --scope <foundry-account-id> -o table`. |
| `claude -p` returns `The model claude-<family>-... is not available on your foundry deployment. Try --model to switch to ...` | Your user-global `~/.claude/settings.json` has `"model"` set to a family this workspace didn't deploy. The postprovision hook writes a workspace `.claude/settings.json` with `"model"` pinned to a deployed family, which overrides the global &mdash; but if you re-ran `azd up` *before* the hook update, or your global has a per-project override, the workspace pin won't apply. Either re-run `pwsh -File scripts/configure-claude-code.ps1` to regenerate `.claude/settings.json`, pick the family explicitly via `claude -p --model <sonnet\|opus\|haiku>`, or edit `~/.claude/settings.json` to remove the `"model"` line. |
| Windows: `UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f60a'` printing the model's response | The Foundry sample apps happily return emoji and other non-CP1252 characters; the default Windows console (cp1252) can't render them. Either set `$env:PYTHONIOENCODING = "utf-8"` before running, or switch the console to UTF-8 with `chcp 65001`. The Python samples already handle this gracefully, but third-party tooling may not. |
| `check_claude_quota.py` exits with `Could not resolve a subscription id ... [WinError 2] The system cannot find the file specified` | The script falls back to `az account show` to find a subscription, but the Azure CLI isn't on `PATH` in the active shell. Either set `$env:AZURE_SUBSCRIPTION_ID = "<sub-id>"` or pass `--subscription <sub-id>` explicitly. |
Expand Down
56 changes: 35 additions & 21 deletions infra-bicep/infra/foundry.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,33 @@ resource project 'Microsoft.CognitiveServices/accounts/projects@2025-10-01-previ
properties: {}
}

// Role assignments are declared BEFORE the model deployments so each
// deployment can dependsOn them. The model-deployment LRO can take
// 30s-20min depending on region and family; chaining the role grants
// first turns that wait into free RBAC propagation time and makes the
// first call after `azd up` succeed without the usual 5-min lag.
// When rbacEnabled is false, both resources are `if(false)` and Bicep
// drops the dependsOn edge automatically.
resource foundryUserAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (rbacEnabled) {
name: guid(account.id, principalId, foundryUserRoleId)
scope: account
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', foundryUserRoleId)
principalId: principalId
principalType: 'User'
}
}

resource foundryProjectManagerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (rbacEnabled) {
name: guid(account.id, principalId, foundryProjectManagerRoleId)
scope: account
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', foundryProjectManagerRoleId)
principalId: principalId
principalType: 'User'
}
}

resource haikuDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-10-01-preview' = if (!empty(haikuModel)) {
parent: account
name: haikuDeploymentNameVar
Expand All @@ -91,6 +118,8 @@ resource haikuDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-
}
dependsOn: [
project
foundryUserAssignment
foundryProjectManagerAssignment
]
}

Expand All @@ -116,10 +145,13 @@ resource sonnetDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025
raiPolicyName: 'Microsoft.DefaultV2'
}
// Foundry serializes deployments under one account; chain them to avoid
// 409s on concurrent create.
// 409s on concurrent create. Role assignments are listed too so the
// first inference call after `azd up` doesn't hit RBAC propagation lag.
dependsOn: [
project
haikuDeployment
foundryUserAssignment
foundryProjectManagerAssignment
]
}

Expand Down Expand Up @@ -147,29 +179,11 @@ resource opusDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-1
dependsOn: [
project
sonnetDeployment
foundryUserAssignment
foundryProjectManagerAssignment
]
}

resource foundryUserAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (rbacEnabled) {
name: guid(account.id, principalId, foundryUserRoleId)
scope: account
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', foundryUserRoleId)
principalId: principalId
principalType: 'User'
}
}

resource foundryProjectManagerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (rbacEnabled) {
name: guid(account.id, principalId, foundryProjectManagerRoleId)
scope: account
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', foundryProjectManagerRoleId)
principalId: principalId
principalType: 'User'
}
}

output claudeBaseUrl string = 'https://${account.name}.services.ai.azure.com/anthropic'
output foundryProjectEndpoint string = 'https://${account.name}.services.ai.azure.com/api/projects/${project.name}'
output foundryAccountName string = account.name
Expand Down
25 changes: 22 additions & 3 deletions infra-terraform/infra/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,16 @@ resource "azapi_resource" "claude_haiku" {
}

response_export_values = ["name"]
depends_on = [azapi_resource.project]
# Chain role assignments first: the role-assignment PUT returns fast (~5s)
# but Foundry data-plane RBAC propagation can take 5+ min. Waiting on the
# model-deployment LRO (30s-20min) in the meantime makes the first call
# after `azd up` work without retries. When ASSIGN_RBAC is false, the
# collections are empty and depends_on is satisfied immediately.
depends_on = [
azapi_resource.project,
azurerm_role_assignment.foundry_user,
azurerm_role_assignment.foundry_project_manager,
]
}

resource "azapi_resource" "claude_sonnet" {
Expand Down Expand Up @@ -166,7 +175,12 @@ resource "azapi_resource" "claude_sonnet" {
}

response_export_values = ["name"]
depends_on = [azapi_resource.project, azapi_resource.claude_haiku]
depends_on = [
azapi_resource.project,
azapi_resource.claude_haiku,
azurerm_role_assignment.foundry_user,
azurerm_role_assignment.foundry_project_manager,
]
}

resource "azapi_resource" "claude_opus" {
Expand Down Expand Up @@ -198,7 +212,12 @@ resource "azapi_resource" "claude_opus" {
}

response_export_values = ["name"]
depends_on = [azapi_resource.project, azapi_resource.claude_sonnet]
depends_on = [
azapi_resource.project,
azapi_resource.claude_sonnet,
azurerm_role_assignment.foundry_user,
azurerm_role_assignment.foundry_project_manager,
]
}

# --- Optional RBAC --------------------------------------------------------
Expand Down
Loading