Repository navigation
Expand file tree
/
Copy pathpreflight-claude.sh
More file actions
237 lines (211 loc) · 11.7 KB
/
Copy pathpreflight-claude.sh
File metadata and controls
237 lines (211 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#!/usr/bin/env bash
# Preflight check for Claude-on-Microsoft-Foundry deployments.
#
# Per-family mode: set any of CLAUDE_HAIKU_MODEL / CLAUDE_SONNET_MODEL /
# CLAUDE_OPUS_MODEL to validate that family. Empty = skip. If all three are
# empty, falls back to CLAUDE_MODEL_NAME (legacy single-model behavior).
#
# Gates `azd up` on:
# 1. Required env vars being set.
# 2. (Informational) Marketplace catalog: each requested model exists in
# the Anthropic-on-Foundry catalog (offer must resolve via
# Microsoft.MarketplaceOrdering). A typo is a hard fail. Agreement
# signed/unsigned is informational (RP auto-signs at deploy time on
# eligible subs).
# 3. **Per-region Cognitive Services quota headroom** per model.
# Hard fail (exit 6) when used + requested > limit. This catches the
# opaque `400 715-123420` error that Terraform's azapi_resource returns
# for quota-rejected requests (azapi bypasses ARM preflight; Bicep /
# `az deployment group create` show the real `InsufficientQuota`).
#
# Exit codes:
# 0 Preflight passed (or skipped — see warnings).
# 4 Marketplace offer not found.
# 6 Insufficient quota (Terraform variant only; on Bicep this is a
# warning because azd's own ARM preflight already surfaces it).
#
# The preflight is best-effort. If CLAUDE_ORGANIZATION_NAME / AZURE_LOCATION
# aren't set, or `az` isn't installed / logged in, it warns and exits 0 so
# `azd up` can continue (azd / Bicep will prompt for any missing parameter;
# the RP surfaces catalog / quota errors at deploy time, just less
# ergonomically). The marketplace-offer check remains a hard fail in both
# variants when it can run. The quota check is a hard fail only on the
# Terraform variant because azapi_resource swallows quota into the opaque
# 715-123420; on Bicep it's a warning because azd's own
# provisionParametersResolver prints InsufficientQuota next and prompts to
# continue.
set -euo pipefail
fail() {
local code="$1"; shift
printf '\nERROR: %s\n\n' "$*" >&2
exit "$code"
}
warn() {
printf 'Preflight: %s\n' "$*" >&2
}
# Detect which IaC variant the preprovision hook is running under. The hook
# always fires from inside the variant folder (infra-bicep/ or
# infra-terraform/), so the local azure.yaml tells us which provider azd is
# about to drive. This decides whether the quota check is a hard fail
# (Terraform: azapi_resource bypasses ARM preflight and the RP returns the
# opaque 400 715-123420, so we MUST catch it here) or a warning (Bicep:
# azd's own provisionParametersResolver already runs ARM preflight and prints
# a clean InsufficientQuota message + prompts to continue, so a hard fail
# here just blocks that better UX).
VARIANT="unknown"
if [ -f "./azure.yaml" ]; then
if grep -qE '^[[:space:]]*provider:[[:space:]]*bicep' ./azure.yaml; then
VARIANT="bicep"
elif grep -qE '^[[:space:]]*provider:[[:space:]]*terraform' ./azure.yaml; then
VARIANT="terraform"
fi
fi
# --- 1. Required env vars ---------------------------------------------------
if [ -z "${CLAUDE_ORGANIZATION_NAME:-}" ]; then
warn "CLAUDE_ORGANIZATION_NAME is not set. azd will prompt for the 'claudeOrganizationName' Bicep parameter at provision time. To skip the prompt: azd env set CLAUDE_ORGANIZATION_NAME 'Your Org'"
fi
if [ -z "${AZURE_LOCATION:-}" ]; then
warn "AZURE_LOCATION is not set. azd will prompt at provision time. Skipping marketplace + quota validation (they need a region). To skip the prompt: azd env set AZURE_LOCATION swedencentral"
exit 0
fi
LOCATION="$AZURE_LOCATION"
# Build the list of (family, model, capacity) tuples.
FAMILIES=()
MODELS=()
CAPACITIES=()
if [ -n "${CLAUDE_HAIKU_MODEL:-}" ]; then
FAMILIES+=("haiku"); MODELS+=("$CLAUDE_HAIKU_MODEL"); CAPACITIES+=("${CLAUDE_HAIKU_CAPACITY:-25}")
fi
if [ -n "${CLAUDE_SONNET_MODEL:-}" ]; then
FAMILIES+=("sonnet"); MODELS+=("$CLAUDE_SONNET_MODEL"); CAPACITIES+=("${CLAUDE_SONNET_CAPACITY:-25}")
fi
if [ -n "${CLAUDE_OPUS_MODEL:-}" ]; then
FAMILIES+=("opus"); MODELS+=("$CLAUDE_OPUS_MODEL"); CAPACITIES+=("${CLAUDE_OPUS_CAPACITY:-25}")
fi
if [ "${#FAMILIES[@]}" -eq 0 ]; then
FAMILIES+=("legacy")
MODELS+=("${CLAUDE_MODEL_NAME:-claude-sonnet-4-6}")
CAPACITIES+=("${CLAUDE_MODEL_CAPACITY:-25}")
fi
# --- 2. Azure CLI / active subscription ------------------------------------
# These checks are best-effort: if `az` is missing or the user hasn't run
# `az login`, we skip the marketplace + quota checks and let `azd up`
# continue. The RP will surface any errors at deploy time.
if ! command -v az >/dev/null 2>&1; then
warn "Azure CLI (az) not found on PATH. Skipping marketplace + quota validation. Install az and run 'az login' for proactive checks: https://learn.microsoft.com/cli/azure/install-azure-cli"
exit 0
fi
SUB_ID="$(az account show --query id -o tsv 2>/dev/null || true)"
if [ -z "$SUB_ID" ]; then
warn "Not signed in to Azure CLI. Skipping marketplace + quota validation. Run 'az login' (and 'az account set --subscription <id>' if needed) for proactive checks."
exit 0
fi
SUMMARY=""
for i in "${!FAMILIES[@]}"; do
SUMMARY="${SUMMARY}${SUMMARY:+, }${FAMILIES[$i]}=${MODELS[$i]}@${CAPACITIES[$i]}"
done
echo "Preflight: subscription $SUB_ID, location $LOCATION, deployments: $SUMMARY"
PUBLISHER="anthropic"
for i in "${!FAMILIES[@]}"; do
FAMILY="${FAMILIES[$i]}"
MODEL_NAME="${MODELS[$i]}"
CAPACITY="${CAPACITIES[$i]}"
# 5.5 offers are not exposed through the legacy MarketplaceOrdering route.
if [[ "$MODEL_NAME" =~ ^claude-(opus|sonnet|haiku)-5-5(-fast)?$ ]]; then
if CATALOG_COUNT="$(az cognitiveservices model list --location "$LOCATION" --query "length([?model.format=='Anthropic' && model.name=='$MODEL_NAME'])" -o tsv 2>&1)" && [[ "$CATALOG_COUNT" =~ ^[0-9]+$ ]]; then
if [ "$CATALOG_COUNT" -eq 0 ]; then
fail 4 "Model '$MODEL_NAME' not found in the Anthropic model catalog for '$LOCATION'. Run Get-ClaudeCatalog.ps1 for available models and regions."
fi
echo "Preflight: '$MODEL_NAME' verified in the regional model catalog."
else
warn "Regional model catalog query did not return a valid count (RP will validate at deploy time): $CATALOG_COUNT"
fi
if [ "$MODEL_NAME" = "claude-opus-5-5-fast" ]; then
warn "'$MODEL_NAME' has no standalone Marketplace offer. The regional model catalog is checked above; the RP validates applicable terms during provisioning."
else
OFFER="$MODEL_NAME-offer"
MP_URL="https://catalogapi.azure.com/offers/anthropic.$OFFER?api-version=2018-08-01-beta&market=US"
if MP_RAW="$(az rest --method get --skip-authorization-header --url "$MP_URL" 2>&1)"; then
echo "Preflight: Marketplace offer '$OFFER' verified. The RP validates terms during provisioning."
else
if echo "$MP_RAW" | grep -qE "was not found|BadRequest|Bad Request|NotFound|Not Found|404"; then
fail 4 "Marketplace offer 'anthropic/$OFFER' not found (family=$FAMILY). Underlying error: $MP_RAW"
fi
warn "Public Marketplace catalog query failed (RP will validate at deploy time): $MP_RAW"
fi
fi
else
OFFER="anthropic-$MODEL_NAME-offer"
PLAN="anthropic-$MODEL_NAME-plan-new"
MP_URL="https://management.azure.com/subscriptions/$SUB_ID/providers/Microsoft.MarketplaceOrdering/offerTypes/virtualmachine/publishers/$PUBLISHER/offers/$OFFER/plans/$PLAN/agreements/current?api-version=2021-01-01"
set +e
MP_RAW="$(az rest --method get --url "$MP_URL" 2>&1)"
MP_EXIT=$?
set -e
if [ "$MP_EXIT" -ne 0 ]; then
if echo "$MP_RAW" | grep -qE "was not found|BadRequest"; then
fail 4 "Marketplace offer 'anthropic/$OFFER/$PLAN' not found (family=$FAMILY).
Likely causes:
- The model id '$MODEL_NAME' is misspelled.
- The model isn't (yet) published in the Anthropic-on-Foundry catalog.
- Anthropic changed the plan naming convention.
Available Anthropic agreements on this subscription:
az rest --method get --url 'https://management.azure.com/subscriptions/$SUB_ID/providers/Microsoft.MarketplaceOrdering/agreements?api-version=2021-01-01' --query \"value[?properties.publisher=='anthropic']\"
Underlying error:
$MP_RAW"
fi
echo "Preflight: Marketplace catalog query for '$MODEL_NAME' returned an unexpected error (continuing — RP will validate at deploy time):" >&2
echo " $MP_RAW" >&2
else
ACCEPTED="$(echo "$MP_RAW" | python -c 'import json, sys; print(json.load(sys.stdin)["properties"]["accepted"])' 2>/dev/null || true)"
if [ "$ACCEPTED" != "True" ] && [ "$ACCEPTED" != "true" ]; then
echo "Preflight: '$MODEL_NAME' marketplace agreement is currently unsigned. The Cognitive Services RP will auto-sign during deployment on eligible subs."
echo " If your subscription blocks RP-initiated subscribes, pre-accept manually:"
echo " az term accept --publisher $PUBLISHER --product $OFFER --plan $PLAN"
else
echo "Preflight: '$MODEL_NAME' marketplace agreement signed."
fi
fi
fi
# --- Quota headroom (HARD FAIL on insufficient) ------------------------
SKU="AIServices.GlobalStandard.$MODEL_NAME"
LIMIT="$(az cognitiveservices usage list --location "$LOCATION" --query "[?name.value=='$SKU'].limit | [0]" -o tsv 2>/dev/null || true)"
CURRENT="$(az cognitiveservices usage list --location "$LOCATION" --query "[?name.value=='$SKU'].currentValue | [0]" -o tsv 2>/dev/null || true)"
if [ -n "$LIMIT" ]; then
LIMIT_INT="${LIMIT%%.*}"
CURRENT_INT="${CURRENT%%.*}"
CURRENT_INT="${CURRENT_INT:-0}"
AVAILABLE=$(( LIMIT_INT - CURRENT_INT ))
if [ "$AVAILABLE" -lt "$CAPACITY" ]; then
FAMILY_UPPER="$(echo "$FAMILY" | tr '[:lower:]' '[:upper:]')"
QUOTA_MSG="Insufficient quota for '$MODEL_NAME' (family=$FAMILY) in '$LOCATION'.
Requested capacity: $CAPACITY TPM (thousands)
Available: $AVAILABLE TPM (limit $LIMIT_INT, currently used $CURRENT_INT)
Fix one of:
- Lower the requested capacity:
azd env set CLAUDE_${FAMILY_UPPER}_CAPACITY $AVAILABLE
(or CLAUDE_MODEL_CAPACITY for legacy single-model mode)
- Free up quota by deleting unused deployments:
az cognitiveservices account deployment list --name <foundry> --resource-group <rg> -o table
az cognitiveservices account deployment delete --name <foundry> --resource-group <rg> --deployment-name <name>
- Request a quota increase in the Azure Foundry portal:
Foundry portal -> Management center -> Quota -> select '$SKU' -> Request increase
Note: without this preflight, Terraform (azapi_resource) would fail with an
opaque '400 715-123420' error because azapi bypasses ARM preflight
validation. Bicep / 'az deployment group create' show the real
'InsufficientQuota' message because they go through ARM preflight."
if [ "$VARIANT" = "bicep" ]; then
# Bicep variant: azd's own ARM preflight will print the same
# InsufficientQuota next and prompt to continue. Warn so the
# diagnostic is up front, then let azd take over.
printf '\nPreflight WARNING: %s\n(Continuing — azd'"'"'s Bicep ARM preflight will repeat this and prompt to continue.)\n\n' "$QUOTA_MSG" >&2
continue
fi
fail 6 "$QUOTA_MSG"
fi
echo "Preflight: '$MODEL_NAME' quota OK ($CAPACITY requested, $AVAILABLE available of $LIMIT_INT in $LOCATION)."
else
echo "Preflight: no quota row visible for '$SKU' in '$LOCATION' yet — first deploy may surface a quota error from the RP." >&2
fi
done
echo "Preflight OK."