feat: dynamic parameter references in usageName metadata for quota-based location filtering#6928
feat: dynamic parameter references in usageName metadata for quota-based location filtering#6928
Conversation
… quota checks (#5432) Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com>
… unnecessary code Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com>
…sed location filtering Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com>
| go 1.25 | ||
|
|
||
| require ( | ||
| github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 |
There was a problem hiding this comment.
@copilot remove all the changes to extensions folder /extensions
This PR should not be updating anything from the extensions folder
There was a problem hiding this comment.
Done — reverted the unrelated go.mod/go.sum changes to extensions/azure.appservice and extensions/microsoft.azd.concurx in commit e219a7c.
There was a problem hiding this comment.
Pull request overview
This PR enables dynamic quota-based location filtering in azd templates by allowing usageName metadata to reference other template parameters (e.g., model name and capacity), then resolving those into SKU usage names via the AI model catalog before performing quota checks.
Changes:
- Added
$(p:paramName)reference parsing/resolution utilities and metadata dependency extraction. - Introduced dependency-aware parameter prompt ordering (topological sort) so referenced parameters are prompted before dependents.
- Implemented SKU usage-name auto-resolution for referenced
usageNameentries, plus supporting docs/tests and changelog entry.
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| cli/azd/pkg/infra/provisioning/bicep/prompt.go | Resolves $(p:...) in usageName, auto-resolves model SKU usage names, and wires resolved usage names into quota-based location filtering. |
| cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go | Adds topo-sorted prompting and a resolvedValues map to support dependency-aware prompting and reference resolution. |
| cli/azd/pkg/infra/provisioning/bicep/prompt_test.go | Updates prompt tests for signature changes and adds tests for topo-sort ordering/error cases. |
| cli/azd/pkg/azure/arm_template.go | Adds HasParamReferences, ExtractParamReferences, ResolveParamReferences, and AzdMetadata.ParamDependencies(). |
| cli/azd/pkg/azure/arm_template_test.go | Adds unit tests for parameter reference detection, extraction, resolution, and dependency collection. |
| cli/azd/docs/design/metadata-parameter-references.md | Design doc describing reference syntax, SKU resolution behavior, ordering, and intended UX/errors. |
| cli/azd/CHANGELOG.md | Documents the new usageName parameter reference feature for location parameters. |
| cli/azd/extensions/microsoft.azd.concurx/go.mod | Updates extension dependencies (large tidy/bump set) not directly tied to this feature. |
| cli/azd/extensions/microsoft.azd.concurx/go.sum | Corresponding dependency checksum updates for concurx extension. |
| cli/azd/extensions/azure.appservice/go.mod | Updates extension dependencies (large tidy/bump set) not directly tied to this feature. |
| cli/azd/extensions/azure.appservice/go.sum | Corresponding dependency checksum updates for azure.appservice extension. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Parse the resolved string as "modelName, capacity" or "modelName" | ||
| parts := strings.SplitN(resolved, ",", 2) | ||
| modelName := strings.TrimSpace(parts[0]) | ||
| capacityStr := "" | ||
| if len(parts) == 2 { | ||
| capacityStr = strings.TrimSpace(parts[1]) | ||
| } | ||
|
|
||
| // Resolve the model name to its full SKU usage name | ||
| skuUsageName, err := p.resolveModelSkuUsageName(ctx, p.env.GetSubscriptionId(), modelName) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("resolving SKU for model '%s': %w", modelName, err) | ||
| } |
There was a problem hiding this comment.
Any presence of $(p:...) triggers “model name → SKU usage name” catalog lookup, even if the template author only intended to reference capacity (e.g. OpenAI.GlobalStandard.gpt-4o, $(p:cap)). In that case, the code will treat the full usage name as a model name and likely fail the catalog lookup. Consider supporting mixed cases by detecting when the resolved first token already looks like a full usage name (or providing an explicit opt-in marker), and skipping SKU auto-resolution in that scenario.
There was a problem hiding this comment.
Applied in commit c9b8afc. After resolving $(p:...) references, the code now checks whether the first token already looks like a full SKU usage name by detecting a dot (Provider.SKU.ModelName format). If so, the AI catalog lookup is skipped and the token is used as-is — so OpenAI.GlobalStandard.gpt-4o, $(p:cap) works correctly. Two new unit tests cover this path.
| // resolveModelSkuUsageName resolves a model name to its full SKU usage name by querying the | ||
| // AI model catalog. It filters SKUs to only standard-tier types (GlobalStandard, DataZoneStandard, | ||
| // Standard), excluding provisioned, batch, and developer SKUs which have fundamentally different | ||
| // billing and usage models. If GlobalStandard is available, it is selected automatically as it | ||
| // offers the highest quota and broadest availability. Otherwise, the user is prompted to choose | ||
| // among the remaining standard-tier SKUs. | ||
| func (p *BicepProvider) resolveModelSkuUsageName( | ||
| ctx context.Context, | ||
| subId string, | ||
| modelName string, | ||
| ) (string, error) { | ||
| if p.aiModelService == nil { | ||
| return "", fmt.Errorf("AI model service is not configured") | ||
| } | ||
|
|
||
| models, err := p.aiModelService.ListModels(ctx, subId, nil) | ||
| if err != nil { | ||
| return "", fmt.Errorf("listing AI models: %w", err) | ||
| } | ||
|
|
||
| // Find the target model and collect unique SKUs across all versions | ||
| type skuInfo struct { | ||
| Name string | ||
| UsageName string | ||
| } | ||
| var allSkus []skuInfo | ||
| seen := map[string]struct{}{} | ||
| modelFound := false | ||
|
|
||
| for _, model := range models { | ||
| if model.Name != modelName { | ||
| continue | ||
| } | ||
| modelFound = true | ||
| for _, version := range model.Versions { | ||
| for _, sku := range version.Skus { | ||
| if _, ok := seen[sku.UsageName]; !ok { | ||
| seen[sku.UsageName] = struct{}{} | ||
| allSkus = append(allSkus, skuInfo{ | ||
| Name: sku.Name, | ||
| UsageName: sku.UsageName, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| break | ||
| } | ||
|
|
||
| if !modelFound { | ||
| return "", fmt.Errorf("model '%s' not found in the AI model catalog", modelName) | ||
| } | ||
| if len(allSkus) == 0 { | ||
| return "", fmt.Errorf("no SKUs found for model '%s'", modelName) | ||
| } | ||
|
|
||
| // Filter to standard-tier SKUs only. | ||
| // Provisioned (PTU-based), Batch (async 24-hr), and Developer SKUs have fundamentally | ||
| // different billing and usage models that are not suitable for typical deployments. | ||
| standardTierSkus := make([]skuInfo, 0, len(allSkus)) | ||
| for _, s := range allSkus { | ||
| switch s.Name { | ||
| case "GlobalStandard", "DataZoneStandard", "Standard": | ||
| standardTierSkus = append(standardTierSkus, s) | ||
| } | ||
| } | ||
|
|
||
| // If no standard-tier SKUs are available, fall back to all SKUs so the user | ||
| // can still proceed (they may have a provisioned deployment in mind). | ||
| candidates := standardTierSkus | ||
| if len(candidates) == 0 { | ||
| candidates = allSkus | ||
| } | ||
|
|
||
| if len(candidates) == 1 { | ||
| return candidates[0].UsageName, nil | ||
| } | ||
|
|
||
| // Auto-select GlobalStandard if available — it has the highest default quota | ||
| // and broadest availability, recommended as the starting point for most workloads. | ||
| for _, s := range candidates { | ||
| if s.Name == "GlobalStandard" { | ||
| return s.UsageName, nil | ||
| } | ||
| } | ||
|
|
||
| // Multiple candidates without GlobalStandard — prompt the user to select one | ||
| options := make([]string, len(candidates)) | ||
| for i, s := range candidates { | ||
| options[i] = fmt.Sprintf("%s (%s)", s.Name, s.UsageName) | ||
| } | ||
|
|
||
| choice, err := p.console.Select(ctx, input.ConsoleOptions{ | ||
| Message: fmt.Sprintf("Select a deployment SKU for model '%s':", modelName), | ||
| Options: options, | ||
| }) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return candidates[choice].UsageName, nil | ||
| } | ||
|
|
||
| // resolveUsageNamesWithReferences resolves usageName entries that contain $(p:...) parameter | ||
| // references. For referenced entries, the resolved value is treated as "modelName, capacity" | ||
| // and the model name is resolved to its full SKU usage name via the AI model catalog. | ||
| // Constant (non-referenced) entries are passed through unchanged. | ||
| func (p *BicepProvider) resolveUsageNamesWithReferences( | ||
| ctx context.Context, | ||
| usageNames []string, | ||
| resolvedValues map[string]any, | ||
| ) ([]string, error) { |
There was a problem hiding this comment.
New behavior adds SKU auto-resolution (resolveModelSkuUsageName) and reference expansion (resolveUsageNamesWithReferences), but there are no unit tests covering these paths (e.g., multiple SKUs prompting, GlobalStandard auto-selection, model-not-found error, and end-to-end quota filtering using $(p:...)). Adding focused tests with a mocked aiModelService and console selection would help prevent regressions and validate the intended UX.
Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com>
…full usage name Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com>
$(p:paramName)references inusageNamemetadata so templates can use user-input model names/capacities for quota-based location filteringpkg/azure/arm_template.gopkg/infra/provisioning/bicep/prompt.gopkg/infra/provisioning/bicep/bicep_provider.gopromptForParameter()to acceptresolvedValues map[string]anypkg/azure/arm_template_test.goandpkg/infra/provisioning/bicep/prompt_test.godocs/design/metadata-parameter-references.mdextensions/folder (azure.appservice and microsoft.azd.concurx go.mod/go.sum)OpenAI.GlobalStandard.gpt-4o), allowing mixed usage likeOpenAI.GlobalStandard.gpt-4o, $(p:cap)prompt_test.goOriginal prompt
Created from VS Code.
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.