From d9a1ea73de97f94c1310bb5813c6211437eaeb82 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 01:27:11 +0000 Subject: [PATCH 1/8] fix: treat empty AI quota usages as available instead of rejecting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the Azure Cognitive Services /usages API returns an empty array (common on free-tier subscriptions that haven't provisioned resources), azd incorrectly rejects all locations as having insufficient quota. The SKU endpoint already confirms model availability in these locations, so empty usage data should mean 0 current consumption — not 0 quota. This fix updates all affected code paths in model_service.go: - ListLocationsWithQuota: include locations with empty usages - ListModelLocationsWithQuota: include locations with empty usages - resolveDeployments: skip quota filter when usageMap is empty - modelHasQuota: return true for models with SKUs when usages empty - maxModelRemainingQuota: return found=true when usages empty Fixes #8533 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/ai/model_helpers_test.go | 4 +- cli/azd/pkg/ai/model_service.go | 48 +++++++++++++++++++++++- cli/azd/pkg/ai/model_service_test.go | 55 +++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/cli/azd/pkg/ai/model_helpers_test.go b/cli/azd/pkg/ai/model_helpers_test.go index c64b824b19d..eaceaf91c12 100644 --- a/cli/azd/pkg/ai/model_helpers_test.go +++ b/cli/azd/pkg/ai/model_helpers_test.go @@ -305,10 +305,10 @@ func TestModelHasQuota(t *testing.T) { expected: true, }, { - name: "no usage entries for model", + name: "no usage entries for model assumes available", usageMap: map[string]AiModelUsage{}, minRemaining: 1, - expected: false, + expected: true, }, { name: "remaining exactly equals min", diff --git a/cli/azd/pkg/ai/model_service.go b/cli/azd/pkg/ai/model_service.go index 8f4d944e068..d9ebe9980fc 100644 --- a/cli/azd/pkg/ai/model_service.go +++ b/cli/azd/pkg/ai/model_service.go @@ -250,6 +250,15 @@ func (s *AiModelService) ListLocationsWithQuota( var results []string sharedResults.Range(func(loc string, usages []*armcognitiveservices.Usage) bool { + // When the /usages API returns an empty list (e.g. free-tier subscriptions + // that have not yet provisioned Cognitive Services resources), treat the + // location as having full quota available. The SKU endpoint already + // confirmed the models are supported in this location. + if len(usages) == 0 { + results = append(results, loc) + return true + } + for _, req := range requirements { minCap := req.MinCapacity if minCap <= 0 { @@ -327,6 +336,17 @@ func (s *AiModelService) ListModelLocationsWithQuota( results := []ModelLocationQuota{} sharedResults.Range(func(loc string, usages []AiModelUsage) bool { + // Empty usage data (e.g. free-tier subscriptions) — treat as + // having sufficient quota since the model catalog already + // confirmed the model is available in this location. + if len(usages) == 0 { + results = append(results, ModelLocationQuota{ + Location: loc, + MaxRemainingQuota: minRemaining, + }) + return true + } + usageMap := make(map[string]AiModelUsage, len(usages)) for _, usage := range usages { usageMap[usage.Name] = usage @@ -469,9 +489,10 @@ func (s *AiModelService) resolveDeployments( continue } - // Quota check + // Quota check — skip when usage data is empty (e.g. free-tier + // subscriptions where the /usages API returns no entries). capacity := ResolveCapacity(sku, options.Capacity) - if quotaOpts != nil && usageMap != nil { + if quotaOpts != nil && usageMap != nil && len(usageMap) > 0 { usage, ok := usageMap[sku.UsageName] if !ok { continue @@ -953,6 +974,17 @@ func ModelHasDefaultVersion(model AiModel) bool { } func modelHasQuota(model AiModel, usageMap map[string]AiModelUsage, minRemaining float64) bool { + // When usage data is empty (e.g. free-tier subscriptions), assume the + // model is eligible as long as it has at least one deployable SKU. + if len(usageMap) == 0 { + for _, version := range model.Versions { + if len(version.Skus) > 0 { + return true + } + } + return false + } + for _, version := range model.Versions { for _, sku := range version.Skus { usage, ok := usageMap[sku.UsageName] @@ -971,6 +1003,18 @@ func modelHasQuota(model AiModel, usageMap map[string]AiModelUsage, minRemaining } func maxModelRemainingQuota(model AiModel, usageMap map[string]AiModelUsage) (float64, bool) { + // When usage data is empty (e.g. free-tier subscriptions), treat the + // model as available if it has at least one SKU. Return -1 to signal + // that the actual remaining quota is unknown. + if len(usageMap) == 0 { + for _, version := range model.Versions { + if len(version.Skus) > 0 { + return -1, true + } + } + return 0, false + } + var maxRemaining float64 found := false for _, version := range model.Versions { diff --git a/cli/azd/pkg/ai/model_service_test.go b/cli/azd/pkg/ai/model_service_test.go index 06f64fa9653..45c0813706d 100644 --- a/cli/azd/pkg/ai/model_service_test.go +++ b/cli/azd/pkg/ai/model_service_test.go @@ -482,10 +482,10 @@ func TestFilterModelsByQuota(t *testing.T) { expected: []string{"gpt-4o"}, }, { - name: "empty usages excludes all models", + name: "empty usages includes models with SKUs", usages: []AiModelUsage{}, minRemaining: 1, - expected: []string{}, + expected: []string{"gpt-4o", "gpt-4o-mini", "text-embedding-ada-002"}, }, } @@ -795,3 +795,54 @@ func TestIsFinetuneUsageName(t *testing.T) { }) } } + +func TestModelHasQuota_EmptyUsages(t *testing.T) { + modelWithSkus := AiModel{ + Name: "gpt-4o", + Versions: []AiModelVersion{ + { + Version: "2024-05-13", + Skus: []AiModelSku{{Name: "Standard", UsageName: "OpenAI.Standard.gpt-4o"}}, + }, + }, + } + modelNoSkus := AiModel{ + Name: "empty-model", + Versions: []AiModelVersion{{Version: "1.0"}}, + } + + emptyUsages := map[string]AiModelUsage{} + + // Model with SKUs should be considered eligible when usage data is empty + require.True(t, modelHasQuota(modelWithSkus, emptyUsages, 1)) + + // Model with no SKUs should not be considered eligible even with empty usage data + require.False(t, modelHasQuota(modelNoSkus, emptyUsages, 1)) +} + +func TestMaxModelRemainingQuota_EmptyUsages(t *testing.T) { + modelWithSkus := AiModel{ + Name: "gpt-4o", + Versions: []AiModelVersion{ + { + Version: "2024-05-13", + Skus: []AiModelSku{{Name: "Standard", UsageName: "OpenAI.Standard.gpt-4o"}}, + }, + }, + } + modelNoSkus := AiModel{ + Name: "empty-model", + Versions: []AiModelVersion{{Version: "1.0"}}, + } + + emptyUsages := map[string]AiModelUsage{} + + // Model with SKUs: found=true, remaining=-1 (unknown) + remaining, found := maxModelRemainingQuota(modelWithSkus, emptyUsages) + require.True(t, found) + require.Equal(t, float64(-1), remaining) + + // Model with no SKUs: found=false + _, found = maxModelRemainingQuota(modelNoSkus, emptyUsages) + require.False(t, found) +} From 37b9820c0bbd4df9a4cfeb075de2c145c9ad30d8 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 01:30:25 +0000 Subject: [PATCH 2/8] fix: also handle empty usages in preflight AI quota check The checkAiModelQuota preflight check has the same issue: when ListUsages returns empty results (free-tier subscriptions), the usageMap lookup returns found=false for every usage name, which is then treated as remaining=0, triggering false 'Insufficient quota' warnings for all model deployments. Skip the quota check for a location when usages are empty, since no consumption data is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index ab64165977f..93013b7dba4 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -2911,6 +2911,15 @@ func (p *BicepProvider) checkAiModelQuota( continue } + // When the /usages API returns an empty list (e.g. free-tier + // subscriptions that have not yet provisioned Cognitive Services + // resources), skip the quota check for this location. Empty + // usages means no consumption data is available — not that + // quota is zero. + if len(usages) == 0 { + continue + } + // Build a lookup map from usage name → remaining quota. usageMap := map[string]float64{} for _, u := range usages { From 8a204a697dcf25e8f42c5cb551e2a6702276d445 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 01:49:26 +0000 Subject: [PATCH 3/8] fix: address Copilot review feedback (iteration 1) - Move empty-usages guard in checkAiModelQuota to only skip the quota comparison, not the model-catalog validation. This preserves ai_model_not_found warnings even when usage data is unavailable. - Use -1 as sentinel for MaxRemainingQuota when usage data is empty instead of minRemaining placeholder. Update prompt_service.go to omit the quota label when the value is unknown (negative). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/internal/grpcserver/prompt_service.go | 11 +++++++++-- cli/azd/pkg/ai/model_service.go | 3 ++- .../provisioning/bicep/bicep_provider.go | 19 ++++++++++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/cli/azd/internal/grpcserver/prompt_service.go b/cli/azd/internal/grpcserver/prompt_service.go index e9134425277..b49c5aae195 100644 --- a/cli/azd/internal/grpcserver/prompt_service.go +++ b/cli/azd/internal/grpcserver/prompt_service.go @@ -1197,8 +1197,15 @@ func (s *promptService) PromptAiModelLocationWithQuota( EnableFiltering: new(true), } for i, loc := range locations { - quotaLabel := output.WithGrayFormat("[up to %.0f quota available]", loc.MaxRemainingQuota) - label := fmt.Sprintf("%s %s", loc.Location, quotaLabel) + var label string + if loc.MaxRemainingQuota < 0 { + label = loc.Location + } else { + quotaLabel := output.WithGrayFormat( + "[up to %.0f quota available]", + loc.MaxRemainingQuota) + label = fmt.Sprintf("%s %s", loc.Location, quotaLabel) + } selectOpts.Choices[i] = &ux.SelectChoice{ Value: loc.Location, Label: label, diff --git a/cli/azd/pkg/ai/model_service.go b/cli/azd/pkg/ai/model_service.go index d9ebe9980fc..68bbd858ef3 100644 --- a/cli/azd/pkg/ai/model_service.go +++ b/cli/azd/pkg/ai/model_service.go @@ -339,10 +339,11 @@ func (s *AiModelService) ListModelLocationsWithQuota( // Empty usage data (e.g. free-tier subscriptions) — treat as // having sufficient quota since the model catalog already // confirmed the model is available in this location. + // Use -1 to signal that the actual remaining quota is unknown. if len(usages) == 0 { results = append(results, ModelLocationQuota{ Location: loc, - MaxRemainingQuota: minRemaining, + MaxRemainingQuota: -1, }) return true } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 93013b7dba4..5d34f6cd413 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -2911,15 +2911,6 @@ func (p *BicepProvider) checkAiModelQuota( continue } - // When the /usages API returns an empty list (e.g. free-tier - // subscriptions that have not yet provisioned Cognitive Services - // resources), skip the quota check for this location. Empty - // usages means no consumption data is available — not that - // quota is zero. - if len(usages) == 0 { - continue - } - // Build a lookup map from usage name → remaining quota. usageMap := map[string]float64{} for _, u := range usages { @@ -3005,6 +2996,16 @@ func (p *BicepProvider) checkAiModelQuota( } // Check aggregated capacity against remaining quota. + // Skip when the /usages API returned an empty list (e.g. free-tier + // subscriptions that have not yet provisioned Cognitive Services + // resources). Empty usages means no consumption data is + // available — not that quota is zero. The model-catalog + // validation above still runs so that ai_model_not_found + // warnings are surfaced regardless. + if len(usages) == 0 { + continue + } + reportedUsage := map[string]bool{} for _, r := range resolved { if reportedUsage[r.usageName] { From 8573f9df304f34d0faafd5c2e41d0734388da3bf Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 01:57:53 +0000 Subject: [PATCH 4/8] fix: address Copilot review feedback (iteration 2) - Reuse maxModelRemainingQuota for empty-usages path in ListModelLocationsWithQuota instead of unconditionally appending. This ensures models with no deployable SKUs are still excluded. - Fix comment wording in ListLocationsWithQuota to accurately describe what the SKU endpoint validates (AIServices/S0 account availability, not specific model support). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/ai/model_service.go | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/cli/azd/pkg/ai/model_service.go b/cli/azd/pkg/ai/model_service.go index 68bbd858ef3..05b6f8d4664 100644 --- a/cli/azd/pkg/ai/model_service.go +++ b/cli/azd/pkg/ai/model_service.go @@ -252,8 +252,9 @@ func (s *AiModelService) ListLocationsWithQuota( sharedResults.Range(func(loc string, usages []*armcognitiveservices.Usage) bool { // When the /usages API returns an empty list (e.g. free-tier subscriptions // that have not yet provisioned Cognitive Services resources), treat the - // location as having full quota available. The SKU endpoint already - // confirmed the models are supported in this location. + // location as having full quota available. The AI Services account SKU + // (AIServices/S0) was already confirmed available in this region; empty + // usages means no consumption data exists, not that quota is zero. if len(usages) == 0 { results = append(results, loc) return true @@ -336,25 +337,20 @@ func (s *AiModelService) ListModelLocationsWithQuota( results := []ModelLocationQuota{} sharedResults.Range(func(loc string, usages []AiModelUsage) bool { - // Empty usage data (e.g. free-tier subscriptions) — treat as - // having sufficient quota since the model catalog already - // confirmed the model is available in this location. - // Use -1 to signal that the actual remaining quota is unknown. - if len(usages) == 0 { - results = append(results, ModelLocationQuota{ - Location: loc, - MaxRemainingQuota: -1, - }) - return true - } - usageMap := make(map[string]AiModelUsage, len(usages)) for _, usage := range usages { usageMap[usage.Name] = usage } - maxRemainingAtLocation, found := maxModelRemainingQuota(*targetModel, usageMap) - if found && maxRemainingAtLocation >= minRemaining { + maxRemainingAtLocation, found := maxModelRemainingQuota( + *targetModel, usageMap) + // Include the location when the model has at least one + // deployable SKU and either: (a) usage data confirms + // sufficient remaining quota, or (b) usage data is + // unavailable (sentinel -1, e.g. free-tier subscriptions). + if found && + (maxRemainingAtLocation < 0 || + maxRemainingAtLocation >= minRemaining) { results = append(results, ModelLocationQuota{ Location: loc, MaxRemainingQuota: maxRemainingAtLocation, From fa769cfc25b7c4cbcb25974b5e27ec8f48578780 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 02:05:13 +0000 Subject: [PATCH 5/8] fix: address Copilot review feedback (iteration 3) - Extract QuotaRemainingUnknown constant (-1) in types.go to centralize the sentinel value for unknown quota. Replace all magic -1 usages across model_service.go and prompt_service.go. - Simplify ListModelLocationsWithQuota by reusing maxModelRemainingQuota for both empty and non-empty usages, ensuring models without deployable SKUs are excluded. - Fix comment wording in ListLocationsWithQuota. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/internal/grpcserver/prompt_service.go | 2 +- cli/azd/pkg/ai/model_service.go | 6 +++--- cli/azd/pkg/ai/model_service_test.go | 2 +- cli/azd/pkg/ai/types.go | 7 +++++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cli/azd/internal/grpcserver/prompt_service.go b/cli/azd/internal/grpcserver/prompt_service.go index b49c5aae195..c4a9cb76ff0 100644 --- a/cli/azd/internal/grpcserver/prompt_service.go +++ b/cli/azd/internal/grpcserver/prompt_service.go @@ -1198,7 +1198,7 @@ func (s *promptService) PromptAiModelLocationWithQuota( } for i, loc := range locations { var label string - if loc.MaxRemainingQuota < 0 { + if loc.MaxRemainingQuota == ai.QuotaRemainingUnknown { label = loc.Location } else { quotaLabel := output.WithGrayFormat( diff --git a/cli/azd/pkg/ai/model_service.go b/cli/azd/pkg/ai/model_service.go index 05b6f8d4664..8b8c71ffdbe 100644 --- a/cli/azd/pkg/ai/model_service.go +++ b/cli/azd/pkg/ai/model_service.go @@ -347,9 +347,9 @@ func (s *AiModelService) ListModelLocationsWithQuota( // Include the location when the model has at least one // deployable SKU and either: (a) usage data confirms // sufficient remaining quota, or (b) usage data is - // unavailable (sentinel -1, e.g. free-tier subscriptions). + // unavailable (e.g. free-tier subscriptions). if found && - (maxRemainingAtLocation < 0 || + (maxRemainingAtLocation == QuotaRemainingUnknown || maxRemainingAtLocation >= minRemaining) { results = append(results, ModelLocationQuota{ Location: loc, @@ -1006,7 +1006,7 @@ func maxModelRemainingQuota(model AiModel, usageMap map[string]AiModelUsage) (fl if len(usageMap) == 0 { for _, version := range model.Versions { if len(version.Skus) > 0 { - return -1, true + return QuotaRemainingUnknown, true } } return 0, false diff --git a/cli/azd/pkg/ai/model_service_test.go b/cli/azd/pkg/ai/model_service_test.go index 45c0813706d..febe03d6f09 100644 --- a/cli/azd/pkg/ai/model_service_test.go +++ b/cli/azd/pkg/ai/model_service_test.go @@ -840,7 +840,7 @@ func TestMaxModelRemainingQuota_EmptyUsages(t *testing.T) { // Model with SKUs: found=true, remaining=-1 (unknown) remaining, found := maxModelRemainingQuota(modelWithSkus, emptyUsages) require.True(t, found) - require.Equal(t, float64(-1), remaining) + require.Equal(t, QuotaRemainingUnknown, remaining) // Model with no SKUs: found=false _, found = maxModelRemainingQuota(modelNoSkus, emptyUsages) diff --git a/cli/azd/pkg/ai/types.go b/cli/azd/pkg/ai/types.go index dbfb32301e0..37ade63ec98 100644 --- a/cli/azd/pkg/ai/types.go +++ b/cli/azd/pkg/ai/types.go @@ -104,9 +104,16 @@ type ModelLocationQuota struct { // Location is the Azure location name. Location string // MaxRemainingQuota is the maximum remaining quota across model SKUs with usage entries. + // A value of QuotaRemainingUnknown (-1) indicates that usage data was unavailable + // (e.g. free-tier subscriptions) and the actual remaining quota is unknown. MaxRemainingQuota float64 } +// QuotaRemainingUnknown is a sentinel value for MaxRemainingQuota indicating that +// the /usages API returned no data (e.g. free-tier subscriptions that have not yet +// provisioned Cognitive Services resources) and the actual remaining quota is unknown. +const QuotaRemainingUnknown float64 = -1 + // QuotaRequirement specifies a single quota check: the usage name to check // and the minimum remaining capacity needed. type QuotaRequirement struct { From 375ed07f64530bb596334c0fd7ba1d29cbe64b27 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 02:11:27 +0000 Subject: [PATCH 6/8] fix: address Copilot review feedback (iteration 4) - Update comment in maxModelRemainingQuota to reference the QuotaRemainingUnknown constant name instead of magic -1. - Document the -1 sentinel semantics in proto comments for max_remaining_quota in both ai_model.proto and prompt.proto. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/grpc/proto/ai_model.proto | 5 ++++- cli/azd/grpc/proto/prompt.proto | 2 ++ cli/azd/pkg/ai/model_service.go | 5 +++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cli/azd/grpc/proto/ai_model.proto b/cli/azd/grpc/proto/ai_model.proto index 263e88a1799..16624708d4f 100644 --- a/cli/azd/grpc/proto/ai_model.proto +++ b/cli/azd/grpc/proto/ai_model.proto @@ -194,7 +194,10 @@ message ListLocationsWithQuotaResponse { message ModelLocationQuota { // Location where model quota was evaluated. Location location = 1; - double max_remaining_quota = 2; // max remaining quota across model SKUs + // Maximum remaining quota across model SKUs. + // A value of -1 indicates that usage data was unavailable (e.g. free-tier + // subscriptions) and the actual remaining quota is unknown. + double max_remaining_quota = 2; } message ListModelLocationsWithQuotaRequest { diff --git a/cli/azd/grpc/proto/prompt.proto b/cli/azd/grpc/proto/prompt.proto index 78bda810ac5..a0b5e6697b3 100644 --- a/cli/azd/grpc/proto/prompt.proto +++ b/cli/azd/grpc/proto/prompt.proto @@ -301,5 +301,7 @@ message PromptAiModelLocationWithQuotaResponse { // Selected location. Location location = 1; // Maximum remaining quota at the selected location across model SKUs. + // A value of -1 indicates that usage data was unavailable (e.g. free-tier + // subscriptions) and the actual remaining quota is unknown. double max_remaining_quota = 2; } diff --git a/cli/azd/pkg/ai/model_service.go b/cli/azd/pkg/ai/model_service.go index 8b8c71ffdbe..01a8739d554 100644 --- a/cli/azd/pkg/ai/model_service.go +++ b/cli/azd/pkg/ai/model_service.go @@ -1001,8 +1001,9 @@ func modelHasQuota(model AiModel, usageMap map[string]AiModelUsage, minRemaining func maxModelRemainingQuota(model AiModel, usageMap map[string]AiModelUsage) (float64, bool) { // When usage data is empty (e.g. free-tier subscriptions), treat the - // model as available if it has at least one SKU. Return -1 to signal - // that the actual remaining quota is unknown. + // model as available if it has at least one SKU. Return + // QuotaRemainingUnknown to signal that the actual remaining quota + // is unknown. if len(usageMap) == 0 { for _, version := range model.Versions { if len(version.Skus) > 0 { From b46e36c3765c46fc28fdbed1c9747a95f642732e Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 02:19:16 +0000 Subject: [PATCH 7/8] fix: address Copilot review feedback (iteration 5) - Update test comment to reference QuotaRemainingUnknown constant instead of magic -1. - Update PR description to match shipped behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/ai/model_service_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/pkg/ai/model_service_test.go b/cli/azd/pkg/ai/model_service_test.go index febe03d6f09..e0fec50ac17 100644 --- a/cli/azd/pkg/ai/model_service_test.go +++ b/cli/azd/pkg/ai/model_service_test.go @@ -837,7 +837,7 @@ func TestMaxModelRemainingQuota_EmptyUsages(t *testing.T) { emptyUsages := map[string]AiModelUsage{} - // Model with SKUs: found=true, remaining=-1 (unknown) + // Model with SKUs: found=true, remaining=QuotaRemainingUnknown remaining, found := maxModelRemainingQuota(modelWithSkus, emptyUsages) require.True(t, found) require.Equal(t, QuotaRemainingUnknown, remaining) From 91920a1a51c5d45b71c43673e357671af64cb2f2 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 5 Jun 2026 17:31:01 +0000 Subject: [PATCH 8/8] fix: scope filterModelsByAnyLocationQuota to per-location models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter models by their declared Locations before applying quota data in filterModelsByAnyLocationQuota. Without this, empty usages from an unrelated location (e.g. free-tier subscription) could mark a model as eligible even when its quota is exhausted in its actual location. Add regression test covering: model only in location A with exhausted quota + empty usages from location B where model is not available → model correctly excluded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/ai/model_service.go | 16 +++++++- cli/azd/pkg/ai/model_service_test.go | 55 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/ai/model_service.go b/cli/azd/pkg/ai/model_service.go index 01a8739d554..91221a8ea62 100644 --- a/cli/azd/pkg/ai/model_service.go +++ b/cli/azd/pkg/ai/model_service.go @@ -1061,8 +1061,20 @@ func filterModelsByAnyLocationQuota( ) []AiModel { eligible := map[string]struct{}{} - for _, usages := range usagesByLocation { - for _, model := range FilterModelsByQuota(models, usages, minRemaining) { + for loc, usages := range usagesByLocation { + // Only consider models that are actually available in this + // location. Without this filter, empty usages from an + // unrelated location could mark a model as eligible even + // when its quota is exhausted in its actual location. + locModels := make([]AiModel, 0, len(models)) + for _, m := range models { + if slices.Contains(m.Locations, loc) { + locModels = append(locModels, m) + } + } + + for _, model := range FilterModelsByQuota( + locModels, usages, minRemaining) { eligible[model.Name] = struct{}{} } } diff --git a/cli/azd/pkg/ai/model_service_test.go b/cli/azd/pkg/ai/model_service_test.go index e0fec50ac17..5fdf759504b 100644 --- a/cli/azd/pkg/ai/model_service_test.go +++ b/cli/azd/pkg/ai/model_service_test.go @@ -775,6 +775,61 @@ func TestFilterModelsByAnyLocationQuota(t *testing.T) { require.Equal(t, []string{"model-a", "model-b"}, filteredNames) } +func TestFilterModelsByAnyLocationQuota_EmptyUsagesDoNotLeakAcrossLocations(t *testing.T) { + // Regression test: a model available only in location A with exhausted + // quota should NOT become eligible because location B returns empty + // usages (e.g. free-tier subscription). + models := []AiModel{ + { + Name: "model-a", + Locations: []string{"eastus"}, + Versions: []AiModelVersion{ + { + Version: "1", + Skus: []AiModelSku{ + {Name: "Standard", UsageName: "a_usage"}, + }, + }, + }, + }, + { + Name: "model-b", + Locations: []string{"eastus", "westus"}, + Versions: []AiModelVersion{ + { + Version: "1", + Skus: []AiModelSku{ + {Name: "Standard", UsageName: "b_usage"}, + }, + }, + }, + }, + } + + usagesByLocation := map[string][]AiModelUsage{ + // eastus: model-a exhausted, model-b has quota + "eastus": { + {Name: "a_usage", CurrentValue: 10, Limit: 10}, + {Name: "b_usage", CurrentValue: 0, Limit: 100}, + }, + // westus: empty usages (free-tier). model-a is NOT + // available here, so it must not become eligible. + "westus": {}, + } + + filtered := filterModelsByAnyLocationQuota( + models, usagesByLocation, 1) + names := make([]string, 0, len(filtered)) + for _, m := range filtered { + names = append(names, m.Name) + } + + // model-a should be excluded (exhausted in eastus, + // not available in westus). + // model-b should be included (has quota in eastus). + require.Equal(t, []string{"model-b"}, names) +} + func TestIsFinetuneUsageName(t *testing.T) { tests := []struct { usageName string