You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: make AI project model deployments per-environment - #10502
After azd env new and provision/up for a hosted agent, model deployment settings must not stay fixed only in azure.yaml in a way that causes InsufficientQuota when the new environment targets another subscription or region. Prefer per-environment deployment config and re-prompt when the current environment lacks values.
Init writes concrete model name, version, SKU, and capacity into azure.yaml. That file is shared across environments. Creating a new environment prompts for subscription and location, but previously still reused the original deployment tuple, so provision often failed with insufficient quota in the new region.
What changed
Keep portable deployment defaults in azure.yaml.
Treat AI_PROJECT_DEPLOYMENTS in the active azd environment as the authoritative list for embedded Foundry provisioning.
azd ai project deployment add seeds or updates that environment list (with rollback on failure).
On preprovision/predeploy for split projects: if the env value is already set, keep it; if missing and interactive, prompt for model/SKU/capacity in the current subscription and location (without pinning the old version/SKU/capacity), then save the full list; if missing under --no-prompt / AZD_NO_PROMPT, save the YAML defaults unchanged. Malformed nonblank values error instead of being overwritten. [] means intentional empty.
Embedded ARM parameters re-read the env list so provision uses the selected deployments, not only the YAML snapshot from Initialize.
Legacy (pre-split) agent projects get the same precedence in the agents extension; split projects remain owned by the projects extension.
Docs note env precedence and non-interactive fallback.
This adapts the closed unmerged PR #7869 idea (AI_PROJECT_DEPLOYMENTS + re-prompt) to current main, where deployments live on azure.ai.project rather than the agent service listen.go path alone. Expandable-string fields in azure.yaml are left as a follow-up; this PR uses the env JSON override with the smallest correct diff.
Tests
Codec round-trip: encoded and raw JSON decode to the same deployments, including quotes/backslashes and integer capacity; blank is missing; [] is present empty; malformed payloads error with guidance to azd env set.
Projects lifecycle precedence: missing/blank interactive prompts using the current env subscription and location without pinning YAML version/SKU/capacity; existing override (raw or escaped) is preserved with no prompt; [] skips prompts; no-prompt writes YAML defaults; malformed values fail without overwrite; prompt cancellation does not write a partial list; ejected on-disk infra skips ineffective prompts and still seeds YAML into the env.
Deployment add: first add seeds the complete YAML list into the env; upsert replaces the named entry and keeps others; unchanged YAML mutation still seeds/updates the env; write failure rolls back YAML and env changes; malformed existing env value fails before mutation.
Foundry provider overlay: with YAML tuple A and env tuple B, final embedded ARM deployments parameters use B (array with numeric capacity); missing/blank keep YAML; [] overrides YAML; malformed fails; on-disk user parameters retain precedence.
Legacy agents path: same missing/set/empty/no-prompt matrix; selection failure does not persist a partial list; split projects with an azure.ai.project host remain a no-op.
Ran (NO_COLOR=1, TERM unset, -race): azure.ai.projects/internal/provisioning, azure.ai.projects/internal/cmd (skipped unrelated Help/Color), and agents UpdateLegacyProjectDeployments* / listen-related tests. All passed. Live Azure quota/provision reproduction not run.
End-to-end testing
Offline run: azd and the azure.ai.projects / azure.ai.agents extensions built from this branch and from main, against a local fake Azure that records the deployment request. Live Azure was not run.
Scenario
Expected
main
This PR
azd ai project deployment add, then replace one deployment
Env stores the full list; replace updates only that entry
When a user runs azd env new and provisions in another subscription or
region, hardcoded model deployment settings in azure.yaml were reused and
often hit InsufficientQuota. Seed and prefer AI_PROJECT_DEPLOYMENTS in the
active azd environment, re-prompt when that value is missing interactively,
and overlay the env list onto embedded Foundry ARM parameters.
FixesAzure#7306
Azure Pipelines:
Successfully started running 3 pipeline(s).
22 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.
Reword README "nonblank" for cspell and use a fixed int32 capacity in
the deployment-add test so gosec G115 no longer fails the lint job.
Hui Miao (huimiu)
changed the title
Make AI project model deployments per-environment
fix: make AI project model deployments per-environment
Oct 16, 2026
Restrict model re-selection to preprovision. This handler also runs on project predeploy (root.go:88–94), and the legacy agents path calls updateLegacyProjectDeployments from predeployHandler (azure.ai.agents/internal/cmd/listen.go:345–350). If AI_PROJECT_DEPLOYMENTS is missing or blank, a standalone azd deploy now requires spare model quota even though it only updates the agent. Existing deployments can consume that quota and block an otherwise valid update. Any replacement selected here is saved but never provisioned by azd deploy. Pass the lifecycle phase into both helpers and keep predeploy validation/seeding-only; test deployment with a missing value and exhausted quota.
When AZURE_SUBSCRIPTION_ID and AZURE_LOCATION are exported in the shell but absent from the azd .env, Foundry initialization accepts them through GetValue. This handler uses GetValues, which returns only persisted values, so the model picker receives an empty subscription and fails with azure_context.scope.subscription_id is required. Resolve the Azure scope through GetValue, while keeping AI_PROJECT_DEPLOYMENTS environment-only. Add coverage for shell-only scope values and update the lifecycle test server to implement GetValue.
When the environment has no override and every reconciliation choice is "Deploy as configured", changed remains false, so the accepted deployment list is never saved. The next interactive provision treats it as missing and prompts again instead of using the accepted version, SKU, and capacity. Seed the environment when !present, include that write in rollback, and report the change so ProjectAddAction refreshes its environment snapshot. Add coverage for this missing-override case.
Apply environment deployment overrides during agent infrastructure initializati…
The supported azd ai agent init --infra path bypasses this override. azure.ai.agents/internal/cmd/init_infra.go:550-633 still synthesizes YAML deployments and writes them directly into Bicep/Terraform parameters. If an environment selected different model settings, ejecting through that command restores the original YAML settings; an explicit [] is lost too. Subsequent provisioning uses those on-disk parameters instead of the environment override. Apply the same decoding and override before the agents ejection writes either format, with regression coverage for nonempty and empty overrides.
Parallel predeploy handlers share one AI_PROJECT_DEPLOYMENTS list.
Hold a process lock across the environment read, prompt, and write so
one handler cannot overwrite the other's selection.
Co-authored-by: Hui Miao <miuhui@outlook.com>
Replacing a project clears AZURE_AI_MODEL_DEPLOYMENT_NAME while keeping
AI_PROJECT_DEPLOYMENTS. Derive the default from that list, use YAML only
when the override is missing, and leave [] empty.
Co-authored-by: Hui Miao <miuhui@outlook.com>
The companion agents-extension ejection path still bypasses this override. azd ai agent init --infra=bicep|terraform calls ejectInfraAfterInit → ejectInfra in azure.ai.agents/internal/cmd/init_infra.go, which passes YAML-synthesized deployments directly to the parameter writers. An active environment's different deployment list—or intentional []—is therefore replaced by YAML defaults in the generated authoritative parameter files. Apply the same decoding and environment precedence there, with tests for both cases.
This helper recognizes ejected infrastructure, not every on-disk template accepted by the provider. For example, a hand-authored .bicep or .bicepparam can satisfy onDiskTemplatePresent without matching an ejection marker or signature. With YAML deployments and no environment override, the hook then prompts for model/quota choices even though resolveTemplate uses the user-owned deployment parameters instead. A selection failure can block otherwise valid provisioning. Align this check with the provider's configured template path/module detection, and cover unmarked on-disk templates.
Use the effective account region for prompts and provisioning
If AZURE_AI_DEPLOYMENTS_LOCATION differs from AZURE_LOCATION, these prompts check model availability and quota in the former, but the embedded provider creates a new Foundry account in the latter (resolveEnv → armParameters). The saved selection can therefore fail with insufficient quota or an unavailable model in the actual account region. Make prompting and provisioning use the same effective account region, and add a test with differing location values.
Resolve Azure scope values from the process environment
With no deployment override and interactive prompting enabled, shell-provided Azure scope values are lost here: GetValues returns only persisted environment values, while the provider's GetValue lookup also accepts process variables. A valid shell-provided subscription/location can therefore become an empty scope in the model prompts and block provisioning. The new legacy-agent prompt path in azure.ai.agents/internal/cmd/listen.go has the same problem. Resolve scope keys through Environment.GetValue in both paths, while keeping AI_PROJECT_DEPLOYMENTS scoped to the persisted environment. Add coverage for scope values supplied only through the process environment.
This detects generated/ejected infrastructure, not every on-disk template the provider accepts. A user-authored infra/main.bicep or main.bicepparam without ejection signatures returns nil here, while onDiskTemplatePresent selects it and resolveTemplate bypasses the environment deployment override. With no saved deployment list, provision prompts for settings that will not be deployed and can fail on an unrelated model or quota lookup. Skip selection whenever the effective template is on disk, using the provider's path and module rules, including layers.
Use the account region for greenfield model selection
For a new embedded Foundry account, this can select models and check quota in a different region from the deployment target. With AZURE_LOCATION=eastus, AZURE_AI_DEPLOYMENTS_LOCATION=westus, and no saved deployment list, the prompts use westus. However, the provider's resolveEnv and armParameters use AZURE_LOCATION for the account, so the saved selection can still fail with insufficient quota or model unavailability in eastus. Use the actual account region for greenfield selection and add a test where these values differ.
Resolve Azure scope from process environment variables
When Azure scope values come from process environment variables rather than .env, Foundry initialization accepts them through Environment.GetValue, but this handler reads only dotenv entries through GetValues. With no saved deployment list, interactive provisioning therefore passes an empty subscription to PromptAiModel and fails with azure_context.scope.subscription_id is required. Resolve the scope keys through GetValue, while keeping AI_PROJECT_DEPLOYMENTS environment-only. Add coverage for process-supplied subscription and location.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goal
After
azd env newand provision/up for a hosted agent, model deployment settings must not stay fixed only inazure.yamlin a way that causesInsufficientQuotawhen the new environment targets another subscription or region. Prefer per-environment deployment config and re-prompt when the current environment lacks values.Fixes #7306
Why
Init writes concrete model name, version, SKU, and capacity into
azure.yaml. That file is shared across environments. Creating a new environment prompts for subscription and location, but previously still reused the original deployment tuple, so provision often failed with insufficient quota in the new region.What changed
azure.yaml.AI_PROJECT_DEPLOYMENTSin the active azd environment as the authoritative list for embedded Foundry provisioning.azd ai project deployment addseeds or updates that environment list (with rollback on failure).--no-prompt/AZD_NO_PROMPT, save the YAML defaults unchanged. Malformed nonblank values error instead of being overwritten.[]means intentional empty.This adapts the closed unmerged PR #7869 idea (
AI_PROJECT_DEPLOYMENTS+ re-prompt) to current main, where deployments live onazure.ai.projectrather than the agent servicelisten.gopath alone. Expandable-string fields inazure.yamlare left as a follow-up; this PR uses the env JSON override with the smallest correct diff.Tests
[]is present empty; malformed payloads error with guidance toazd env set.[]skips prompts; no-prompt writes YAML defaults; malformed values fail without overwrite; prompt cancellation does not write a partial list; ejected on-disk infra skips ineffective prompts and still seeds YAML into the env.deploymentsparameters use B (array with numeric capacity); missing/blank keep YAML;[]overrides YAML; malformed fails; on-disk user parameters retain precedence.azure.ai.projecthost remain a no-op.-race):azure.ai.projects/internal/provisioning,azure.ai.projects/internal/cmd(skipped unrelated Help/Color), and agentsUpdateLegacyProjectDeployments*/ listen-related tests. All passed. Live Azure quota/provision reproduction not run.End-to-end testing
Offline run: azd and the
azure.ai.projects/azure.ai.agentsextensions built from this branch and frommain, against a local fake Azure that records the deployment request. Live Azure was not run.mainazd ai project deployment add, then replace one deployment--no-promptazure.yamldefaultsazure.yaml(#7306)azure.yamllistazure.yamlcapacity reusedNote: on the legacy Bicep path the env value must use the escaped form azd writes; plain JSON breaks Bicep parameter parsing (same on
main).