diff --git a/azure.yaml b/azure.yaml index fbf47cd..1245b60 100644 --- a/azure.yaml +++ b/azure.yaml @@ -21,3 +21,19 @@ deployment: AzureAiServiceLocation: ${{ parameters.AzureAiServiceLocation }} Prefix: ${{ parameters.Prefix }} baseUrl: ${{ parameters.baseUrl }} +hooks: + preprovision: + posix: + shell: sh + run: > + chmod u+r+x ./scripts/validate_model_deployment_quota.sh; chmod u+r+x ./scripts/validate_model_quota.sh; ./scripts/validate_model_deployment_quota.sh --subscription "$AZURE_SUBSCRIPTION_ID" --location "${AZURE_AISERVICE_LOCATION:-japaneast}" --models-parameter "aiModelDeployments" + interactive: false + continueOnError: false + + windows: + shell: pwsh + run: > + $location = if ($env:AZURE_AISERVICE_LOCATION) { $env:AZURE_AISERVICE_LOCATION } else { "japaneast" }; + ./scripts/validate_model_deployment_quota.ps1 -SubscriptionId $env:AZURE_SUBSCRIPTION_ID -Location $location -ModelsParameter "aiModelDeployments" + interactive: false + continueOnError: false \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index 913134a..b6e2947 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -4,28 +4,19 @@ param Prefix string var abbrs = loadJsonContent('./abbreviations.json') var safePrefix = length(Prefix) > 20 ? substring(Prefix, 0, 20) : Prefix +@description('Required. Location for all Resources except AI Foundry.') +param solutionLocation string = resourceGroup().location + @allowed([ 'australiaeast' - 'brazilsouth' - 'canadacentral' - 'canadaeast' 'eastus' 'eastus2' 'francecentral' - 'germanywestcentral' 'japaneast' - 'koreacentral' - 'northcentralus' 'norwayeast' - 'polandcentral' - 'southafricanorth' - 'southcentralus' 'southindia' 'swedencentral' - 'switzerlandnorth' - 'uaenorth' 'uksouth' - 'westeurope' 'westus' 'westus3' ]) @@ -57,8 +48,6 @@ param gptModelVersion string = '2024-08-06' var uniqueId = toLower(uniqueString(subscription().id, safePrefix, resourceGroup().location)) var UniquePrefix = 'cm${padLeft(take(uniqueId, 12), 12, '0')}' var ResourcePrefix = take('cm${safePrefix}${UniquePrefix}', 15) -var location = resourceGroup().location -var dblocation = resourceGroup().location var cosmosdbDatabase = 'cmsadb' var cosmosdbBatchContainer = 'cmsabatch' var cosmosdbFileContainer = 'cmsafile' @@ -85,7 +74,7 @@ var aiModelDeployments = [ resource azureAiServices 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' = { name: azureAiServicesName - location: location + location: AzureAiServiceLocation sku: { name: 'S0' } @@ -121,7 +110,7 @@ module managedIdentityModule 'deploy_managed_identity.bicep' = { params: { miName:'${abbrs.security.managedIdentity}${ResourcePrefix}' solutionName: ResourcePrefix - solutionLocation: location + solutionLocation: solutionLocation } scope: resourceGroup(resourceGroup().name) } @@ -133,7 +122,7 @@ module kvault 'deploy_keyvault.bicep' = { params: { keyvaultName: '${abbrs.security.keyVault}${ResourcePrefix}' solutionName: ResourcePrefix - solutionLocation: location + solutionLocation: solutionLocation managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId } scope: resourceGroup(resourceGroup().name) @@ -162,7 +151,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.9.1 params: { logAnalyticsWorkspaceResourceId: azureAifoundry.outputs.logAnalyticsId name: toLower('${ResourcePrefix}manenv') - location: location + location: solutionLocation zoneRedundant: false managedIdentities: managedIdentityModule } @@ -175,7 +164,7 @@ module databaseAccount 'br/public:avm/res/document-db/database-account:0.9.0' = name: toLower('${abbrs.databases.cosmosDBDatabase}${ResourcePrefix}databaseAccount') // Non-required parameters enableAnalyticalStorage: true - location: dblocation + location: solutionLocation managedIdentities: { systemAssigned: true userAssignedResourceIds: [ @@ -193,7 +182,7 @@ module databaseAccount 'br/public:avm/res/document-db/database-account:0.9.0' = { failoverPriority: 0 isZoneRedundant: false - locationName: dblocation + locationName: solutionLocation } ] sqlDatabases: [ @@ -268,14 +257,14 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.13.0' = { environmentResourceId: containerAppsEnvironment.outputs.resourceId name: toLower('${abbrs.containers.containerApp}${ResourcePrefix}Frontend') // Non-required parameters - location: location + location: solutionLocation } } resource containerAppBackend 'Microsoft.App/containerApps@2023-05-01' = { name: toLower('${abbrs.containers.containerApp}${ResourcePrefix}Backend') - location: location + location: solutionLocation identity: { type: 'SystemAssigned' } @@ -393,7 +382,7 @@ resource containerAppBackend 'Microsoft.App/containerApps@2023-05-01' = { } resource storageContianerApp 'Microsoft.Storage/storageAccounts@2022-09-01' = { name: storageContainerName - location: location + location: solutionLocation sku: { name: storageSkuName } diff --git a/infra/main.bicepparam b/infra/main.bicepparam index e10d646..1bb741e 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -1,7 +1,8 @@ using './main.bicep' param Prefix = readEnvironmentVariable('AZURE_ENV_NAME','azdtemp') -param AzureAiServiceLocation = readEnvironmentVariable('AZURE_LOCATION','japaneast') +param solutionLocation = readEnvironmentVariable('AZURE_LOCATION', 'eastus2') +param AzureAiServiceLocation = readEnvironmentVariable('AZURE_AISERVICE_LOCATION','japaneast') param capacity = int(readEnvironmentVariable('AZURE_ENV_MODEL_CAPACITY', '200')) param deploymentType = readEnvironmentVariable('AZURE_ENV_MODEL_DEPLOYMENT_TYPE', 'GlobalStandard') param llmModel = readEnvironmentVariable('AZURE_ENV_MODEL_NAME', 'gpt-4o') diff --git a/infra/main.json b/infra/main.json index 6e93844..ab72560 100644 --- a/infra/main.json +++ b/infra/main.json @@ -16,30 +16,25 @@ "description": "Prefix for all resources created by this template. This should be 3-20 characters long. If your provide a prefix longer than 20 characters, it will be truncated to 20 characters." } }, + "solutionLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Required. Location for all Resources except AI Foundry." + } + }, "AzureAiServiceLocation": { "type": "string", "allowedValues": [ "australiaeast", - "brazilsouth", - "canadacentral", - "canadaeast", "eastus", "eastus2", "francecentral", - "germanywestcentral", "japaneast", - "koreacentral", - "northcentralus", "norwayeast", - "polandcentral", - "southafricanorth", - "southcentralus", "southindia", "swedencentral", - "switzerlandnorth", - "uaenorth", "uksouth", - "westeurope", "westus", "westus3" ], @@ -49,7 +44,43 @@ }, "capacity": { "type": "int", - "defaultValue": 5 + "defaultValue": 5, + "minValue": 5, + "metadata": { + "description": "Capacity of the GPT deployment:" + } + }, + "deploymentType": { + "type": "string", + "defaultValue": "GlobalStandard", + "minLength": 1, + "metadata": { + "description": "GPT model deployment type:" + } + }, + "llmModel": { + "type": "string", + "defaultValue": "gpt-4o", + "minLength": 1, + "metadata": { + "description": "Name of the GPT model to deploy:" + } + }, + "imageVersion": { + "type": "string", + "defaultValue": "latest", + "minLength": 1, + "metadata": { + "description": "Set the Image tag:" + } + }, + "gptModelVersion": { + "type": "string", + "defaultValue": "2024-08-06", + "minLength": 1, + "metadata": { + "description": "Version of the GPT model to deploy:" + } } }, "variables": { @@ -285,27 +316,21 @@ "uniqueId": "[toLower(uniqueString(subscription().id, variables('safePrefix'), resourceGroup().location))]", "UniquePrefix": "[format('cm{0}', padLeft(take(variables('uniqueId'), 12), 12, '0'))]", "ResourcePrefix": "[take(format('cm{0}{1}', variables('safePrefix'), variables('UniquePrefix')), 15)]", - "imageVersion": "latest", - "location": "[resourceGroup().location]", - "dblocation": "[resourceGroup().location]", "cosmosdbDatabase": "cmsadb", "cosmosdbBatchContainer": "cmsabatch", "cosmosdbFileContainer": "cmsafile", "cosmosdbLogContainer": "cmsalog", - "deploymentType": "GlobalStandard", "containerName": "appstorage", - "llmModel": "gpt-4o", "storageSkuName": "Standard_LRS", "storageContainerName": "[replace(replace(replace(replace(format('{0}cast', variables('ResourcePrefix')), '-', ''), '_', ''), '.', ''), '/', '')]", - "gptModelVersion": "2024-08-06", "azureAiServicesName": "[format('{0}{1}', variables('abbrs').ai.aiServices, variables('ResourcePrefix'))]", "aiModelDeployments": [ { - "name": "[variables('llmModel')]", - "model": "[variables('llmModel')]", - "version": "[variables('gptModelVersion')]", + "name": "[parameters('llmModel')]", + "model": "[parameters('llmModel')]", + "version": "[parameters('gptModelVersion')]", "sku": { - "name": "[variables('deploymentType')]", + "name": "[parameters('deploymentType')]", "capacity": "[parameters('capacity')]" }, "raiPolicyName": "Microsoft.Default" @@ -321,7 +346,7 @@ "type": "Microsoft.CognitiveServices/accounts", "apiVersion": "2024-04-01-preview", "name": "[variables('azureAiServicesName')]", - "location": "[variables('location')]", + "location": "[parameters('AzureAiServiceLocation')]", "sku": { "name": "S0" }, @@ -360,7 +385,7 @@ "type": "Microsoft.App/containerApps", "apiVersion": "2023-05-01", "name": "[toLower(format('{0}{1}Backend', variables('abbrs').containers.containerApp, variables('ResourcePrefix')))]", - "location": "[variables('location')]", + "location": "[parameters('solutionLocation')]", "identity": { "type": "SystemAssigned" }, @@ -380,7 +405,7 @@ "containers": [ { "name": "cmsabackend", - "image": "[format('cmsacontainerreg.azurecr.io/cmsabackend:{0}', variables('imageVersion'))]", + "image": "[format('cmsacontainerreg.azurecr.io/cmsabackend:{0}', parameters('imageVersion'))]", "env": [ { "name": "COSMOSDB_ENDPOINT", @@ -420,35 +445,35 @@ }, { "name": "MIGRATOR_AGENT_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "PICKER_AGENT_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "FIXER_AGENT_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "SEMANTIC_VERIFIER_AGENT_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "SYNTAX_CHECKER_AGENT_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "SELECTION_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "TERMINATION_MODEL_DEPLOY", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME", - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, { "name": "AZURE_AI_AGENT_PROJECT_NAME", @@ -486,7 +511,7 @@ "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2022-09-01", "name": "[variables('storageContainerName')]", - "location": "[variables('location')]", + "location": "[parameters('solutionLocation')]", "sku": { "name": "[variables('storageSkuName')]" }, @@ -609,7 +634,7 @@ "value": "[variables('ResourcePrefix')]" }, "solutionLocation": { - "value": "[variables('location')]" + "value": "[parameters('solutionLocation')]" } }, "template": { @@ -706,7 +731,7 @@ "value": "[variables('ResourcePrefix')]" }, "solutionLocation": { - "value": "[variables('location')]" + "value": "[parameters('solutionLocation')]" }, "managedIdentityObjectId": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId]" @@ -830,10 +855,10 @@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_keyvault'), '2022-09-01').outputs.keyvaultName.value]" }, "gptModelName": { - "value": "[variables('llmModel')]" + "value": "[parameters('llmModel')]" }, "gptModelVersion": { - "value": "[variables('gptModelVersion')]" + "value": "[parameters('gptModelVersion')]" }, "managedIdentityObjectId": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId]" @@ -1489,7 +1514,7 @@ "value": "[toLower(format('{0}manenv', variables('ResourcePrefix')))]" }, "location": { - "value": "[variables('location')]" + "value": "[parameters('solutionLocation')]" }, "zoneRedundant": { "value": false @@ -2108,7 +2133,7 @@ "value": true }, "location": { - "value": "[variables('dblocation')]" + "value": "[parameters('solutionLocation')]" }, "managedIdentities": { "value": { @@ -2134,7 +2159,7 @@ { "failoverPriority": 0, "isZoneRedundant": false, - "locationName": "[variables('dblocation')]" + "locationName": "[parameters('solutionLocation')]" } ] }, @@ -5934,7 +5959,7 @@ "value": "[format('https://{0}', reference(resourceId('Microsoft.App/containerApps', toLower(format('{0}{1}Backend', variables('abbrs').containers.containerApp, variables('ResourcePrefix')))), '2023-05-01').configuration.ingress.fqdn)]" } ], - "image": "[format('cmsacontainerreg.azurecr.io/cmsafrontend:{0}', variables('imageVersion'))]", + "image": "[format('cmsacontainerreg.azurecr.io/cmsafrontend:{0}', parameters('imageVersion'))]", "name": "cmsafrontend", "resources": { "cpu": "1", @@ -5962,7 +5987,7 @@ "value": "[toLower(format('{0}{1}Frontend', variables('abbrs').containers.containerApp, variables('ResourcePrefix')))]" }, "location": { - "value": "[variables('location')]" + "value": "[parameters('solutionLocation')]" } }, "template": { diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 112bae1..81b1a25 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -51,6 +51,22 @@ }, "principalId": { "value": "${AZURE_PRINCIPAL_ID}" - } + }, + "aiModelDeployments": { + "value": [ + { + "name": "gpt-4o", + "model": { + "name": "gpt-4o", + "version": "2024-08-06", + "format": "OpenAI" + }, + "sku": { + "name": "GlobalStandard", + "capacity": 200 + } + } + ] + } } } diff --git a/scripts/quota_check_params.sh b/scripts/quota_check_params.sh index 5787cb2..fa10db2 100644 --- a/scripts/quota_check_params.sh +++ b/scripts/quota_check_params.sh @@ -91,7 +91,7 @@ az account set --subscription "$AZURE_SUBSCRIPTION_ID" echo "šŸŽÆ Active Subscription: $(az account show --query '[name, id]' --output tsv)" # Default Regions to check (Comma-separated, now configurable) -DEFAULT_REGIONS="eastus,uksouth,eastus2,northcentralus,swedencentral,westus,westus2,southcentralus,canadacentral" +DEFAULT_REGIONS="australiaeast,eastus,eastus2,francecentral,japaneast,norwayeast,southindia,swedencentral,uksouth,westus,westus3" IFS=',' read -r -a DEFAULT_REGION_ARRAY <<< "$DEFAULT_REGIONS" # Read parameters (if any) diff --git a/scripts/validate_model_deployment_quota.ps1 b/scripts/validate_model_deployment_quota.ps1 new file mode 100644 index 0000000..007442f --- /dev/null +++ b/scripts/validate_model_deployment_quota.ps1 @@ -0,0 +1,73 @@ +param ( + [string]$SubscriptionId, + [string]$Location, + [string]$ModelsParameter +) + +# Verify all required parameters are provided +$MissingParams = @() + +if (-not $SubscriptionId) { + $MissingParams += "subscription" +} + +if (-not $Location) { + $MissingParams += "location" +} + +if (-not $ModelsParameter) { + $MissingParams += "models-parameter" +} + +if ($MissingParams.Count -gt 0) { + Write-Error "āŒ ERROR: Missing required parameters: $($MissingParams -join ', ')" + Write-Host "Usage: .\validate_model_deployment_quotas.ps1 -SubscriptionId -Location -ModelsParameter " + exit 1 +} + +$JsonContent = Get-Content -Path "./infra/main.parameters.json" -Raw | ConvertFrom-Json + +if (-not $JsonContent) { + Write-Error "āŒ ERROR: Failed to parse main.parameters.json. Ensure the JSON file is valid." + exit 1 +} + +$aiModelDeployments = $JsonContent.parameters.$ModelsParameter.value + +if (-not $aiModelDeployments -or -not ($aiModelDeployments -is [System.Collections.IEnumerable])) { + Write-Error "āŒ ERROR: The specified property $ModelsParameter does not exist or is not an array." + exit 1 +} + +az account set --subscription $SubscriptionId +Write-Host "šŸŽÆ Active Subscription: $(az account show --query '[name, id]' --output tsv)" + +$QuotaAvailable = $true + +foreach ($deployment in $aiModelDeployments) { + $name = if ($env:AZURE_ENV_MODEL_NAME) { $env:AZURE_ENV_MODEL_NAME } else { $deployment.name } + $model = if ($env:AZURE_ENV_MODEL_NAME) { $env:AZURE_ENV_MODEL_NAME } else { $deployment.model.name } + $type = if ($env:AZURE_ENV_MODEL_DEPLOYMENT_TYPE) { $env:AZURE_ENV_MODEL_DEPLOYMENT_TYPE } else { $deployment.sku.name } + $capacity = if ($env:AZURE_ENV_MODEL_CAPACITY) { $env:AZURE_ENV_MODEL_CAPACITY } else { $deployment.sku.capacity } + + Write-Host "`nšŸ” Validating model deployment: $name ..." + & .\scripts\validate_model_quota.ps1 -Location $Location -Model $model -Capacity $capacity -DeploymentType $type + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + if ($exitCode -eq 2) { + # Quota error already printed inside the script, exit gracefully without reprinting + exit 1 + } + Write-Error "āŒ ERROR: Quota validation failed for model deployment: $name" + $QuotaAvailable = $false + } +} + +if (-not $QuotaAvailable) { + Write-Error "āŒ ERROR: One or more model deployments failed validation." + exit 1 +} else { + Write-Host "āœ… All model deployments passed quota validation successfully." + exit 0 +} \ No newline at end of file diff --git a/scripts/validate_model_deployment_quota.sh b/scripts/validate_model_deployment_quota.sh new file mode 100644 index 0000000..3a19cfb --- /dev/null +++ b/scripts/validate_model_deployment_quota.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +SUBSCRIPTION_ID="" +LOCATION="" +MODELS_PARAMETER="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --subscription) + SUBSCRIPTION_ID="$2" + shift 2 + ;; + --location) + LOCATION="$2" + shift 2 + ;; + --models-parameter) + MODELS_PARAMETER="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Verify all required parameters are provided and echo missing ones +MISSING_PARAMS=() + +if [[ -z "$SUBSCRIPTION_ID" ]]; then + MISSING_PARAMS+=("subscription") +fi + +if [[ -z "$LOCATION" ]]; then + MISSING_PARAMS+=("location") +fi + +if [[ -z "$MODELS_PARAMETER" ]]; then + MISSING_PARAMS+=("models-parameter") +fi + +if [[ ${#MISSING_PARAMS[@]} -ne 0 ]]; then + echo "āŒ ERROR: Missing required parameters: ${MISSING_PARAMS[*]}" + echo "Usage: $0 --subscription --location --models-parameter " + exit 1 +fi + +aiModelDeployments=$(jq -c ".parameters.$MODELS_PARAMETER.value[]" ./infra/main.parameters.json) + +if [ $? -ne 0 ]; then + echo "Error: Failed to parse main.parameters.json. Ensure jq is installed and the JSON file is valid." + exit 1 +fi + +az account set --subscription "$SUBSCRIPTION_ID" +echo "šŸŽÆ Active Subscription: $(az account show --query '[name, id]' --output tsv)" + +quotaAvailable=true + +while IFS= read -r deployment; do + name=${AZURE_ENV_MODEL_NAME:-$(echo "$deployment" | jq -r '.name')} + model=${AZURE_ENV_MODEL_NAME:-$(echo "$deployment" | jq -r '.model.name')} + type=${AZURE_ENV_MODEL_DEPLOYMENT_TYPE:-$(echo "$deployment" | jq -r '.sku.name')} + capacity=${AZURE_ENV_MODEL_CAPACITY:-$(echo "$deployment" | jq -r '.sku.capacity')} + + echo "šŸ” Validating model deployment: $name ..." + ./scripts/validate_model_quota.sh --location "$LOCATION" --model "$model" --capacity $capacity --deployment-type $type + + # Check if the script failed + exit_code=$? + if [ $exit_code -ne 0 ]; then + if [ $exit_code -eq 2 ]; then + # Skip printing any quota validation error — already handled inside the validation script + exit 1 + fi + echo "āŒ ERROR: Quota validation failed for model deployment: $name" + quotaAvailable=false + fi +done <<< "$(echo "$aiModelDeployments")" + +if [ "$quotaAvailable" = false ]; then + echo "āŒ ERROR: One or more model deployments failed validation." + exit 1 +else + echo "āœ… All model deployments passed quota validation successfully." + exit 0 +fi diff --git a/scripts/validate_model_quota.ps1 b/scripts/validate_model_quota.ps1 new file mode 100644 index 0000000..ef10596 --- /dev/null +++ b/scripts/validate_model_quota.ps1 @@ -0,0 +1,108 @@ +param ( + [string]$Location, + [string]$Model, + [string]$DeploymentType = "Standard", + [int]$Capacity +) + +# Verify required parameters +$MissingParams = @() +if (-not $Location) { $MissingParams += "location" } +if (-not $Model) { $MissingParams += "model" } +if (-not $Capacity) { $MissingParams += "capacity" } +if (-not $DeploymentType) { $MissingParams += "deployment-type" } + +if ($MissingParams.Count -gt 0) { + Write-Error "āŒ ERROR: Missing required parameters: $($MissingParams -join ', ')" + Write-Host "Usage: .\validate_model_quota.ps1 -Location -Model -Capacity [-DeploymentType ]" + exit 1 +} + +if ($DeploymentType -ne "Standard" -and $DeploymentType -ne "GlobalStandard") { + Write-Error "āŒ ERROR: Invalid deployment type: $DeploymentType. Allowed values are 'Standard' or 'GlobalStandard'." + exit 1 +} + +$ModelType = "OpenAI.$DeploymentType.$Model" + +$PreferredRegions = @('australiaeast', 'eastus', 'eastus2', 'francecentral', 'japaneast', 'norwayeast', 'southindia', 'swedencentral', 'uksouth', 'westus', 'westus3') +$AllResults = @() + +function Check-Quota { + param ( + [string]$Region + ) + + $ModelInfoRaw = az cognitiveservices usage list --location $Region --query "[?name.value=='$ModelType']" --output json + $ModelInfo = $null + + try { + $ModelInfo = $ModelInfoRaw | ConvertFrom-Json + } catch { + return + } + + if (-not $ModelInfo) { + return + } + + $CurrentValue = ($ModelInfo | Where-Object { $_.name.value -eq $ModelType }).currentValue + $Limit = ($ModelInfo | Where-Object { $_.name.value -eq $ModelType }).limit + + $CurrentValue = [int]($CurrentValue -replace '\.0+$', '') + $Limit = [int]($Limit -replace '\.0+$', '') + $Available = $Limit - $CurrentValue + + $script:AllResults += [PSCustomObject]@{ + Region = $Region + Model = $ModelType + Limit = $Limit + Used = $CurrentValue + Available = $Available + } +} + +foreach ($region in $PreferredRegions) { + Check-Quota -Region $region +} + +# Display Results Table +Write-Host "\n-------------------------------------------------------------------------------------------------------------" +Write-Host "| No. | Region | Model Name | Limit | Used | Available |" +Write-Host "-------------------------------------------------------------------------------------------------------------" + +$count = 1 +foreach ($entry in $AllResults) { + $index = $PreferredRegions.IndexOf($entry.Region) + 1 + $modelShort = $entry.Model.Substring($entry.Model.LastIndexOf(".") + 1) + Write-Host ("| {0,-4} | {1,-16} | {2,-35} | {3,-7} | {4,-7} | {5,-9} |" -f $index, $entry.Region, $entry.Model, $entry.Limit, $entry.Used, $entry.Available) + $count++ +} +Write-Host "-------------------------------------------------------------------------------------------------------------" + +$EligibleRegion = $AllResults | Where-Object { $_.Region -eq $Location -and $_.Available -ge $Capacity } +if ($EligibleRegion) { + Write-Host "\nāœ… Sufficient quota found in original region '$Location'." + exit 0 +} + +$FallbackRegions = $AllResults | Where-Object { $_.Region -ne $Location -and $_.Available -ge $Capacity } + +if ($FallbackRegions.Count -gt 0) { + Write-Host "`nāŒ Deployment cannot proceed because the original region '$Location' lacks sufficient quota." + Write-Host "āž”ļø You can retry using one of the following regions with sufficient quota:`n" + + foreach ($region in $FallbackRegions) { + Write-Host " • $($region.Region) (Available: $($region.Available))" + } + + Write-Host "`nšŸ”§ To proceed, run:" + Write-Host " azd env set AZURE_AISERVICE_LOCATION ''" + Write-Host "šŸ“Œ To confirm it's set correctly, run:" + Write-Host " azd env get-value AZURE_AISERVICE_LOCATION" + Write-Host "ā–¶ļø Once confirmed, re-run azd up to deploy the model in the new region." + exit 2 +} + +Write-Error "āŒ ERROR: No available quota found in any region." +exit 1 \ No newline at end of file diff --git a/scripts/validate_model_quota.sh b/scripts/validate_model_quota.sh new file mode 100644 index 0000000..b855fbf --- /dev/null +++ b/scripts/validate_model_quota.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +LOCATION="" +MODEL="" +DEPLOYMENT_TYPE="Standard" +CAPACITY=0 + +ALL_REGIONS=('australiaeast' 'eastus' 'eastus2' 'francecentral' 'japaneast' 'norwayeast' 'southindia' 'swedencentral' 'uksouth' 'westus' 'westus3') + +while [[ $# -gt 0 ]]; do + case "$1" in + --model) + MODEL="$2" + shift 2 + ;; + --capacity) + CAPACITY="$2" + shift 2 + ;; + --deployment-type) + DEPLOYMENT_TYPE="$2" + shift 2 + ;; + --location) + LOCATION="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Validate required params +MISSING_PARAMS=() +[[ -z "$LOCATION" ]] && MISSING_PARAMS+=("location") +[[ -z "$MODEL" ]] && MISSING_PARAMS+=("model") +[[ -z "$CAPACITY" ]] && MISSING_PARAMS+=("capacity") + +if [[ ${#MISSING_PARAMS[@]} -ne 0 ]]; then + echo "āŒ ERROR: Missing required parameters: ${MISSING_PARAMS[*]}" + echo "Usage: $0 --location --model --capacity [--deployment-type ]" + exit 1 +fi + +if [[ "$DEPLOYMENT_TYPE" != "Standard" && "$DEPLOYMENT_TYPE" != "GlobalStandard" ]]; then + echo "āŒ ERROR: Invalid deployment type: $DEPLOYMENT_TYPE. Allowed values are 'Standard' or 'GlobalStandard'." + exit 1 +fi + +MODEL_TYPE="OpenAI.$DEPLOYMENT_TYPE.$MODEL" + +declare -a FALLBACK_REGIONS=() +ROW_NO=1 + +printf "\n%-5s | %-20s | %-40s | %-10s | %-10s | %-10s\n" "No." "Region" "Model Name" "Limit" "Used" "Available" +printf -- "---------------------------------------------------------------------------------------------------------------------\n" + +for region in "${ALL_REGIONS[@]}"; do + MODEL_INFO=$(az cognitiveservices usage list --location "$region" --query "[?name.value=='$MODEL_TYPE']" --output json 2>/dev/null) + + if [[ -n "$MODEL_INFO" && "$MODEL_INFO" != "[]" ]]; then + CURRENT_VALUE=$(echo "$MODEL_INFO" | jq -r '.[0].currentValue // 0' | cut -d'.' -f1) + LIMIT=$(echo "$MODEL_INFO" | jq -r '.[0].limit // 0' | cut -d'.' -f1) + AVAILABLE=$((LIMIT - CURRENT_VALUE)) + + printf "%-5s | %-20s | %-40s | %-10s | %-10s | %-10s\n" "$ROW_NO" "$region" "$MODEL_TYPE" "$LIMIT" "$CURRENT_VALUE" "$AVAILABLE" + + if [[ "$region" == "$LOCATION" && "$AVAILABLE" -ge "$CAPACITY" ]]; then + echo -e "\nāœ… Sufficient quota available in user-specified region: $LOCATION" + exit 0 + fi + + if [[ "$region" != "$LOCATION" && "$AVAILABLE" -ge "$CAPACITY" ]]; then + FALLBACK_REGIONS+=("$region ($AVAILABLE)") + fi + fi + + ((ROW_NO++)) +done + +printf -- "---------------------------------------------------------------------------------------------------------------------\n" + +if [[ "${#FALLBACK_REGIONS[@]}" -gt 0 ]]; then + echo -e "\nāŒ Deployment cannot proceed because the original region '$LOCATION' lacks sufficient quota." + echo "āž”ļø You can retry using one of the following regions with sufficient quota:" + for fallback in "${FALLBACK_REGIONS[@]}"; do + echo " • $fallback" + done + echo -e "\nšŸ”§ To proceed, run:" + echo " azd env set AZURE_AISERVICE_LOCATION ''" + echo "šŸ“Œ To confirm it's set correctly, run:" + echo " azd env get-value AZURE_AISERVICE_LOCATION" + echo "ā–¶ļø Once confirmed, re-run azd up to deploy the model in the new region." + exit 2 +fi + +echo "āŒ ERROR: No available quota found in any of the fallback regions." +exit 1 \ No newline at end of file