Skip to content

fix: make AI project model deployments per-environment - #10502

Open
Hui Miao (huimiu) wants to merge 7 commits into
Azure:mainfrom
huimiu:fix/env-scoped-agent-deployments
Open

Hui Miao (huimiu) wants to merge 7 commits into
Azure:mainfrom
huimiu:fix/env-scoped-agent-deployments

Conversation

@huimiu

@huimiu Hui Miao (huimiu) commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Goal

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.

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

  • 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 Env not written Pass
New env, no value, --no-prompt Uses azure.yaml defaults Pass Pass
Env has its own model / SKU / capacity Env value is deployed Overwritten by azure.yaml (#7306) Pass
Malformed env value Clear error before any Azure call; value unchanged Silently replaced Pass
Several envs, switch back to the first Each env keeps and deploys its own list All share the azure.yaml list Pass
Interactive provision in a new env Prompts for model / version / SKU / capacity for that region, saves it, no prompt on rerun No prompt; azure.yaml capacity reused Pass
Legacy agents project (Bicep) Same rules as above Env value overwritten Pass

Note: on the legacy Bicep path the env value must use the escaped form azd writes; plain JSON breaks Bicep parameter parsing (same on main).

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.

Fixes Azure#7306
@azure-pipelines

Copy link
Copy Markdown
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.

@github-actions github-actions Bot added ext-agents azure.ai.agents extension ext-foundry azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes}, microsoft.foundry ext-projects azure.ai.projects extension labels Oct 15, 2026
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Region selection, infrastructure ejection, and existing-project adoption can use or restore incorrect deployment settings.

4 open findings
What changed in this PR

Makes Foundry model deployments environment-specific to reduce quota failures when switching subscriptions or regions.

Changes:

  • Adds environment deployment overrides, model selection, and non-interactive defaults.
  • Persists deployment-add changes with rollback and applies overrides during embedded provisioning.
  • Adds regression coverage and documents precedence.
File Description
cli/​azd/​extensions/​azure.ai.projects/​README.md Documents deployment precedence.
cli/​azd/​extensions/​azure.ai.projects/​internal/​provisioning/​project_deployments.go Adds encoding, decoding, and selection helpers.
cli/​azd/​extensions/​azure.ai.projects/​internal/​provisioning/​project_deployments_test.go Tests encoding and validation.
cli/​azd/​extensions/​azure.ai.projects/​internal/​provisioning/​foundry_provisioning_provider.go Applies environment deployment overrides.
cli/​azd/​extensions/​azure.ai.projects/​internal/​provisioning/​foundry_provisioning_provider_test.go Tests overrides and existing-project reuse.
cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go Resolves lifecycle deployment settings.
cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config_test.go Tests lifecycle precedence and selection.
cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_deployment_add.go Persists deployment lists with rollback.
cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_deployment_add_test.go Tests persistence and rollback.
cli/​azd/​extensions/​azure.ai.agents/​tests/​cli-interactive-tester-scenarios/​tier1b/​1b.01-deploy-template-python.yaml Checks seeded settings prevent repeat prompts.
cli/​azd/​extensions/​azure.ai.agents/​internal/​cmd/​listen.go Adds legacy environment precedence.
cli/​azd/​extensions/​azure.ai.agents/​internal/​cmd/​listen_test.go Tests legacy precedence and split-project isolation.
cli/​azd/​extensions/​ai-non-interactive.md Documents non-interactive deployment defaults.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Legacy migration and project service config were checking quota in
AZURE_LOCATION even when the account region was recorded separately.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 09:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Azure scope fallback and provisioning-only model selection can block otherwise valid provisioning or agent deployment workflows.

2 open findings
2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Restrict model reselection to preprovision

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:82

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.

Medium severity Resolve Azure scope from environment-aware values

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:88

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.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 16, 2026 09:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Deployment state can still be lost or overwritten during project replacement, adoption, agents ejection, and concurrent legacy handlers.

2 open findings
2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Persist accepted deployments when the environment override is missing

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_deployment_reconcile.go:263

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.

Medium severity Apply environment deployment overrides during agent infrastructure initializati…

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_existing_infra.go:988

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.

🧠 Review effort: Balanced

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Comment thread cli/azd/extensions/azure.ai.projects/internal/cmd/project_add.go Outdated
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>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 16:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Adoption can still remove shared deployment defaults, and scope resolution and infrastructure handling leave provisioning inconsistencies.

1 open finding
2 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Honor environment deployment overrides during agent infrastructure ejection

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_existing_infra.go:993

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.

Medium severity Detect all supported on-disk deployment templates

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:100

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.

Medium severity Use the effective account region for prompts and provisioning

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:108

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.

Medium severity Resolve Azure scope values from the process environment

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:113

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.

🧠 Review effort: Balanced

Comment thread cli/azd/extensions/azure.ai.projects/internal/cmd/project_deployment_reconcile.go Outdated
Comment thread cli/azd/extensions/azure.ai.projects/internal/cmd/project_add.go
Comment thread cli/azd/extensions/azure.ai.projects/README.md
Comment thread cli/azd/extensions/azure.ai.projects/internal/provisioning/project_deployments.go Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved scope resolution, template detection, and empty-override handling can reject valid workflows or select deployments for the wrong region.

1 open issue
3 previously missed issues

In code that hasn't changed since last review

Medium severity Respect all on-disk provider templates during selection

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:104

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.

Medium severity Use the account region for greenfield model selection

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:108

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.

Medium severity Resolve Azure scope from process environment variables

cli/​azd/​extensions/​azure.ai.projects/​internal/​cmd/​project_service_config.go:108

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.

1 resolved since last review

Review effort: Balanced

Comment on lines +1452 to 1462
deployments, _, err := provisioning.DecodeProjectDeployments(values[projectDeploymentsEnvKey])
if err != nil {
return err
}
sameEndpoint := false
hasDeployments := len(deployments) > 0
if service != nil {
sameEndpoint = equalProjectEndpoint(serviceEndpoint(service.Resolved), endpoint)
hasDeployments = hasDeployments || hasManagedDeployments(service.Resolved) ||
hasManagedDeployments(service.Raw)
}
@azure-sdk-automation

Copy link
Copy Markdown
Contributor

azure.ai.agents PR build

Note

This is an unsigned development build. Install it only if you trust this PR.

Install the extension:

azd ext install "https://azuresdkartifacts.z5.web.core.windows.net/azd/extensions/pr/10502/azure-ai-agents.zip"
  • Version: 1.0.0-beta.20.pr.10502.6970313
  • Merge commit: 9b3a943

@azure-sdk-automation

Copy link
Copy Markdown
Contributor

azure.ai.projects PR build

Note

This is an unsigned development build. Install it only if you trust this PR.

Install the extension:

azd ext install "https://azuresdkartifacts.z5.web.core.windows.net/azd/extensions/pr/10502/azure-ai-projects.zip"
  • Version: 1.0.0-beta.14.pr.10502.6970315
  • Merge commit: 9b3a943

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-agents azure.ai.agents extension ext-foundry azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes}, microsoft.foundry ext-projects azure.ai.projects extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Your subscription has insufficient quota for this resource when creating a new environment and trying to provision hosted agent

4 participants