From 2a324e86f10ab6e4ec4a8a13d13869ecbb24af8f Mon Sep 17 00:00:00 2001 From: Paul Yuknewicz Date: Mon, 21 Sep 2026 17:27:23 -0700 Subject: [PATCH 1/2] Fix AI Gateway model route provisioning Use the current token-limit counter key shape and allow the runtime model route enough time to become available after ARM provisioning completes.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 ++ infra/ai-gateway/main.bicep | 4 +++- infra/scripts/configure-ai-gateway.ps1 | 10 ++++++---- infra/scripts/configure-ai-gateway.sh | 10 ++++++---- tests/test-ai-gateway-model-registration.sh | 9 +++++++++ 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7053b21..4e0daaf 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ azd ai agent invoke "Create a concise daily repo digest for microsoft/agent-fram If Azure leaves a failed AI Gateway after an activation or managed-identity conflict, rerun `azd up`. The preprovision hook deletes only the terminal-Failed `AIGateway` tagged for the current azd environment, purges its APIM soft-delete record, and waits for identity cleanup. `azd down` performs the same bounded purge and settle process in its postdown hook. If the bound expires, follow the exact retry guidance printed by the hook rather than relying on name availability alone. +The postprovision hook can wait up to five minutes for a newly registered model route to become available after ARM provisioning succeeds. + Run the scheduled digest immediately: ```bash diff --git a/infra/ai-gateway/main.bicep b/infra/ai-gateway/main.bicep index 01e7068..6e2fd06 100644 --- a/infra/ai-gateway/main.bicep +++ b/infra/ai-gateway/main.bicep @@ -149,7 +149,9 @@ resource foundryCatalogModels 'Microsoft.ApiManagement/service/workspaces/modelP type: 'tokenLimit' period: 'minute' count: model.?tokenLimit ?? 10000 - counterKey: 'Identity' + counterKey: [ + 'identity' + ] } ] } diff --git a/infra/scripts/configure-ai-gateway.ps1 b/infra/scripts/configure-ai-gateway.ps1 index 0db0b19..a4f78cf 100644 --- a/infra/scripts/configure-ai-gateway.ps1 +++ b/infra/scripts/configure-ai-gateway.ps1 @@ -4,6 +4,8 @@ $PSNativeCommandUseErrorActionPreference = $true $aiGatewayApiVersion = "2025-09-01-preview" $legacyAiGatewayApiVersion = "2026-05-01" $foundryUserRoleId = "53ca6127-db72-4b80-b1b0-d745d6d5456d" +$modelRouteMaxAttempts = 60 +$modelRouteRetrySeconds = 5 $defaultRepository = "microsoft/agent-framework" $githubMcpServer = "https://api.githubcopilot.com/mcp/" $githubMcpTools = "list_pull_requests,list_issues,actions_list" @@ -103,7 +105,7 @@ function Test-GatewayModelRoute($GatewayUrl, $Model, $ApiKey) { } | ConvertTo-Json -Depth 5 -Compress $lastError = "" - for ($attempt = 1; $attempt -le 15; $attempt++) { + for ($attempt = 1; $attempt -le $modelRouteMaxAttempts; $attempt++) { try { Invoke-RestMethod ` -Method Post ` @@ -115,14 +117,14 @@ function Test-GatewayModelRoute($GatewayUrl, $Model, $ApiKey) { return } catch { $lastError = $_.Exception.Message - if ($attempt -lt 15) { + if ($attempt -lt $modelRouteMaxAttempts) { Write-Host "Waiting for the AI Gateway model route, attempt=$attempt" - Start-Sleep -Seconds 4 + Start-Sleep -Seconds $modelRouteRetrySeconds } } } - throw "AI Gateway model route failed. The route requires an explicit Api-Key header. Last error: $lastError" + throw "AI Gateway model route did not become ready after $modelRouteMaxAttempts attempts. Last error: $lastError" } function Prepare-BicepRbac { diff --git a/infra/scripts/configure-ai-gateway.sh b/infra/scripts/configure-ai-gateway.sh index 930929a..389191b 100755 --- a/infra/scripts/configure-ai-gateway.sh +++ b/infra/scripts/configure-ai-gateway.sh @@ -5,6 +5,8 @@ umask 077 AI_GATEWAY_API_VERSION="2025-09-01-preview" LEGACY_AI_GATEWAY_API_VERSION="2026-05-01" FOUNDRY_USER_ROLE_ID="53ca6127-db72-4b80-b1b0-d745d6d5456d" +MODEL_ROUTE_MAX_ATTEMPTS=60 +MODEL_ROUTE_RETRY_SECONDS=5 DEFAULT_REPOSITORY="microsoft/agent-framework" GITHUB_MCP_SERVER="https://api.githubcopilot.com/mcp/" GITHUB_MCP_TOOLS="list_pull_requests,list_issues,actions_list" @@ -88,7 +90,7 @@ verify_gateway_model_route() { request_body="$(python3 -c 'import json,sys; print(json.dumps({"model": sys.argv[1], "messages": [{"role": "user", "content": "Reply with exactly one word: ok"}], "max_completion_tokens": 128}))' "$model")" response_file="$(mktemp)" - for attempt in $(seq 1 15); do + for attempt in $(seq 1 "$MODEL_ROUTE_MAX_ATTEMPTS"); do status="$(curl -sS \ -o "$response_file" \ -w '%{http_code}' \ @@ -105,13 +107,13 @@ verify_gateway_model_route() { ;; esac - if [ "$attempt" -lt 15 ]; then + if [ "$attempt" -lt "$MODEL_ROUTE_MAX_ATTEMPTS" ]; then echo "Waiting for the AI Gateway model route, attempt=${attempt}, status=${status:-unavailable}" - sleep 4 + sleep "$MODEL_ROUTE_RETRY_SECONDS" fi done - echo "AI Gateway model route failed with HTTP ${status:-unavailable}. The route requires an explicit Api-Key header." >&2 + echo "AI Gateway model route did not become ready after ${MODEL_ROUTE_MAX_ATTEMPTS} attempts (HTTP ${status:-unavailable})." >&2 if [ -s "$response_file" ]; then cat "$response_file" >&2 echo >&2 diff --git a/tests/test-ai-gateway-model-registration.sh b/tests/test-ai-gateway-model-registration.sh index 67016d2..9b866ae 100755 --- a/tests/test-ai-gateway-model-registration.sh +++ b/tests/test-ai-gateway-model-registration.sh @@ -39,6 +39,15 @@ if [[ "$actual_token_limit" != *"$expected_token_limit"* ]]; then exit 1 fi +expected_counter_key="'counterKey', createArray('identity')" + +if [[ "$actual_token_limit" != *"$expected_counter_key"* ]]; then + echo "Gateway token limit counterKey must use the current array contract." >&2 + echo "Expected expression containing: $expected_counter_key" >&2 + echo "Actual: $actual_token_limit" >&2 + exit 1 +fi + az bicep build \ --file "$repo_root/infra/main.bicep" \ --outfile "$compiled_root_template" \ From 03fcd3788aea06cf6000af19890f8164303348d5 Mon Sep 17 00:00:00 2001 From: Paul Yuknewicz Date: Tue, 22 Sep 2026 16:46:28 -0700 Subject: [PATCH 2/2] Provision AI Gateway model aliases Create the default-workspace aliases required for runtime model routing and keep route readiness validation bounded.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- IMPLEMENTATION_NOTES.md | 4 ++++ README.md | 2 -- infra/ai-gateway/main.bicep | 14 ++++++++++++++ infra/scripts/configure-ai-gateway.ps1 | 10 ++++------ infra/scripts/configure-ai-gateway.sh | 10 ++++------ tests/test-ai-gateway-model-registration.sh | 21 +++++++++++++++++++++ 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/IMPLEMENTATION_NOTES.md b/IMPLEMENTATION_NOTES.md index a60c90f..adb8c75 100644 --- a/IMPLEMENTATION_NOTES.md +++ b/IMPLEMENTATION_NOTES.md @@ -306,6 +306,10 @@ Each model registration includes: - `deployment.modelName` set to the Foundry deployment name from the final segment of the deployment resource ID +Each registration also has a default-workspace alias that maps the OpenAI +`model` value to the provider model resource. The runtime model route is not +created when the provider model exists without this alias. + The deployment name, not the backing catalog model name, is the OpenAI `model` value sent through AI Gateway. diff --git a/README.md b/README.md index 4e0daaf..7053b21 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,6 @@ azd ai agent invoke "Create a concise daily repo digest for microsoft/agent-fram If Azure leaves a failed AI Gateway after an activation or managed-identity conflict, rerun `azd up`. The preprovision hook deletes only the terminal-Failed `AIGateway` tagged for the current azd environment, purges its APIM soft-delete record, and waits for identity cleanup. `azd down` performs the same bounded purge and settle process in its postdown hook. If the bound expires, follow the exact retry guidance printed by the hook rather than relying on name availability alone. -The postprovision hook can wait up to five minutes for a newly registered model route to become available after ARM provisioning succeeds. - Run the scheduled digest immediately: ```bash diff --git a/infra/ai-gateway/main.bicep b/infra/ai-gateway/main.bicep index 6e2fd06..7f558d0 100644 --- a/infra/ai-gateway/main.bicep +++ b/infra/ai-gateway/main.bicep @@ -157,6 +157,20 @@ resource foundryCatalogModels 'Microsoft.ApiManagement/service/workspaces/modelP } }] +resource foundryModelAliases 'Microsoft.ApiManagement/service/workspaces/aliases@2025-09-01-preview' = [for (model, i) in foundryModels: if (deployFoundryProvider && deployApimGatewayViaBicep) { + parent: defaultWorkspace + name: model.armName + properties: { + aliasName: last(split(model.resourceId, '/')) + description: model.?description ?? null + backendModels: [ + { + resourceId: foundryCatalogModels[i].id + } + ] + } +}] + resource toolServers 'Microsoft.ApiManagement/service/workspaces/toolServers@2025-09-01-preview' = [for config in items(toolServerConfigs): if (deployApimGatewayViaBicep) { parent: defaultWorkspace name: config.key diff --git a/infra/scripts/configure-ai-gateway.ps1 b/infra/scripts/configure-ai-gateway.ps1 index a4f78cf..8b34ae1 100644 --- a/infra/scripts/configure-ai-gateway.ps1 +++ b/infra/scripts/configure-ai-gateway.ps1 @@ -4,8 +4,6 @@ $PSNativeCommandUseErrorActionPreference = $true $aiGatewayApiVersion = "2025-09-01-preview" $legacyAiGatewayApiVersion = "2026-05-01" $foundryUserRoleId = "53ca6127-db72-4b80-b1b0-d745d6d5456d" -$modelRouteMaxAttempts = 60 -$modelRouteRetrySeconds = 5 $defaultRepository = "microsoft/agent-framework" $githubMcpServer = "https://api.githubcopilot.com/mcp/" $githubMcpTools = "list_pull_requests,list_issues,actions_list" @@ -105,7 +103,7 @@ function Test-GatewayModelRoute($GatewayUrl, $Model, $ApiKey) { } | ConvertTo-Json -Depth 5 -Compress $lastError = "" - for ($attempt = 1; $attempt -le $modelRouteMaxAttempts; $attempt++) { + for ($attempt = 1; $attempt -le 15; $attempt++) { try { Invoke-RestMethod ` -Method Post ` @@ -117,14 +115,14 @@ function Test-GatewayModelRoute($GatewayUrl, $Model, $ApiKey) { return } catch { $lastError = $_.Exception.Message - if ($attempt -lt $modelRouteMaxAttempts) { + if ($attempt -lt 15) { Write-Host "Waiting for the AI Gateway model route, attempt=$attempt" - Start-Sleep -Seconds $modelRouteRetrySeconds + Start-Sleep -Seconds 4 } } } - throw "AI Gateway model route did not become ready after $modelRouteMaxAttempts attempts. Last error: $lastError" + throw "AI Gateway model route did not become ready after 15 attempts. Last error: $lastError" } function Prepare-BicepRbac { diff --git a/infra/scripts/configure-ai-gateway.sh b/infra/scripts/configure-ai-gateway.sh index 389191b..a8cbf88 100755 --- a/infra/scripts/configure-ai-gateway.sh +++ b/infra/scripts/configure-ai-gateway.sh @@ -5,8 +5,6 @@ umask 077 AI_GATEWAY_API_VERSION="2025-09-01-preview" LEGACY_AI_GATEWAY_API_VERSION="2026-05-01" FOUNDRY_USER_ROLE_ID="53ca6127-db72-4b80-b1b0-d745d6d5456d" -MODEL_ROUTE_MAX_ATTEMPTS=60 -MODEL_ROUTE_RETRY_SECONDS=5 DEFAULT_REPOSITORY="microsoft/agent-framework" GITHUB_MCP_SERVER="https://api.githubcopilot.com/mcp/" GITHUB_MCP_TOOLS="list_pull_requests,list_issues,actions_list" @@ -90,7 +88,7 @@ verify_gateway_model_route() { request_body="$(python3 -c 'import json,sys; print(json.dumps({"model": sys.argv[1], "messages": [{"role": "user", "content": "Reply with exactly one word: ok"}], "max_completion_tokens": 128}))' "$model")" response_file="$(mktemp)" - for attempt in $(seq 1 "$MODEL_ROUTE_MAX_ATTEMPTS"); do + for attempt in $(seq 1 15); do status="$(curl -sS \ -o "$response_file" \ -w '%{http_code}' \ @@ -107,13 +105,13 @@ verify_gateway_model_route() { ;; esac - if [ "$attempt" -lt "$MODEL_ROUTE_MAX_ATTEMPTS" ]; then + if [ "$attempt" -lt 15 ]; then echo "Waiting for the AI Gateway model route, attempt=${attempt}, status=${status:-unavailable}" - sleep "$MODEL_ROUTE_RETRY_SECONDS" + sleep 4 fi done - echo "AI Gateway model route did not become ready after ${MODEL_ROUTE_MAX_ATTEMPTS} attempts (HTTP ${status:-unavailable})." >&2 + echo "AI Gateway model route did not become ready after 15 attempts (HTTP ${status:-unavailable})." >&2 if [ -s "$response_file" ]; then cat "$response_file" >&2 echo >&2 diff --git a/tests/test-ai-gateway-model-registration.sh b/tests/test-ai-gateway-model-registration.sh index 9b866ae..00ae1af 100755 --- a/tests/test-ai-gateway-model-registration.sh +++ b/tests/test-ai-gateway-model-registration.sh @@ -48,6 +48,27 @@ if [[ "$actual_token_limit" != *"$expected_counter_key"* ]]; then exit 1 fi +alias_properties="$(jq -r ' + .resources[] + | select(.type == "Microsoft.ApiManagement/service/workspaces/aliases") + | .properties +' "$compiled_template")" +expected_alias_name="last(split(parameters('foundryModels')[copyIndex()].resourceId, '/'))" +expected_backend_model="Microsoft.ApiManagement/service/workspaces/modelProviders/models" + +if [[ "$alias_properties" != *"$expected_alias_name"* ]]; then + echo "Gateway model alias must use the Foundry deployment name." >&2 + echo "Expected expression containing: $expected_alias_name" >&2 + echo "Actual: $alias_properties" >&2 + exit 1 +fi + +if [[ "$alias_properties" != *"$expected_backend_model"* ]]; then + echo "Gateway model alias must target the registered provider model." >&2 + echo "Actual: $alias_properties" >&2 + exit 1 +fi + az bicep build \ --file "$repo_root/infra/main.bicep" \ --outfile "$compiled_root_template" \