@@ -18,6 +18,7 @@ import (
1818 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
1919 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
2020 "github.com/azure/azure-dev/cli/azd/pkg/azdext"
21+ "github.com/azure/azure-dev/cli/azd/pkg/output"
2122 "github.com/azure/azure-dev/cli/azd/pkg/ux"
2223 "google.golang.org/grpc/codes"
2324 "google.golang.org/grpc/status"
@@ -734,23 +735,27 @@ func ensureLocation(
734735 azureContext * azdext.AzureContext ,
735736 envName string ,
736737) error {
737- if azureContext .Scope .Location != "" {
738+ allowedLocations := supportedRegionsForInit ()
739+
740+ if azureContext .Scope .Location != "" && locationAllowed (azureContext .Scope .Location , allowedLocations ) {
738741 return nil
739742 }
743+ if azureContext .Scope .Location != "" {
744+ fmt .Printf ("%s" , output .WithWarningFormat (
745+ "The current AZURE_LOCATION '%s' is not supported for this agent setup. Please choose a different location.\n " ,
746+ azureContext .Scope .Location ,
747+ ))
748+ azureContext .Scope .Location = ""
749+ }
740750
741751 fmt .Println ("Select an Azure location. This determines which models are available and where your Foundry project resources will be deployed." )
742752
743- locationResponse , err := azdClient .Prompt ().PromptLocation (ctx , & azdext.PromptLocationRequest {
744- AzureContext : azureContext ,
745- })
753+ locationName , err := promptLocationForInit (ctx , azdClient , azureContext , allowedLocations )
746754 if err != nil {
747- if exterrors .IsCancellation (err ) {
748- return exterrors .Cancelled ("location selection was cancelled" )
749- }
750- return exterrors .FromPrompt (err , "failed to prompt for location" )
755+ return err
751756 }
752757
753- azureContext .Scope .Location = locationResponse . Location . Name
758+ azureContext .Scope .Location = locationName
754759
755760 return setEnvValue (ctx , azdClient , envName , "AZURE_LOCATION" , azureContext .Scope .Location )
756761}
@@ -776,6 +781,57 @@ func ensureSubscriptionAndLocation(
776781 return newCredential , nil
777782}
778783
784+ func normalizeLocationName (location string ) string {
785+ return strings .TrimSpace (strings .ToLower (location ))
786+ }
787+
788+ func locationAllowed (location string , allowedLocations []string ) bool {
789+ if len (allowedLocations ) == 0 {
790+ return true
791+ }
792+
793+ normalized := normalizeLocationName (location )
794+ return slices .ContainsFunc (allowedLocations , func (allowed string ) bool {
795+ return normalized == normalizeLocationName (allowed )
796+ })
797+ }
798+
799+ func promptLocationForInit (
800+ ctx context.Context ,
801+ azdClient * azdext.AzdClient ,
802+ azureContext * azdext.AzureContext ,
803+ allowedLocations []string ,
804+ ) (string , error ) {
805+ locationResponse , err := azdClient .Prompt ().PromptLocation (ctx , & azdext.PromptLocationRequest {
806+ AzureContext : azureContext ,
807+ AllowedLocations : allowedLocations ,
808+ })
809+ if err != nil {
810+ if exterrors .IsCancellation (err ) {
811+ return "" , exterrors .Cancelled ("location selection was cancelled" )
812+ }
813+ return "" , exterrors .FromPrompt (err , "failed to prompt for location" )
814+ }
815+
816+ return locationResponse .Location .Name , nil
817+ }
818+
819+ func agentModelFilter (locations []string , excludeModelNames []string ) * azdext.AiModelFilterOptions {
820+ filter := & azdext.AiModelFilterOptions {
821+ Capabilities : []string {agentsV2ModelCapability },
822+ }
823+
824+ if len (locations ) > 0 {
825+ filter .Locations = locations
826+ }
827+
828+ if len (excludeModelNames ) > 0 {
829+ filter .ExcludeModelNames = excludeModelNames
830+ }
831+
832+ return filter
833+ }
834+
779835// --- Shared model helpers ---
780836
781837// selectNewModel prompts the user to select a model from the AI catalog, filtered by location.
@@ -793,15 +849,13 @@ func selectNewModel(
793849
794850 promptReq := & azdext.PromptAiModelRequest {
795851 AzureContext : azureContext ,
852+ Filter : agentModelFilter ([]string {azureContext .Scope .Location }, nil ),
796853 SelectOptions : & azdext.SelectOptions {
797854 Message : "Select a model" ,
798855 },
799856 Quota : & azdext.QuotaCheckOptions {
800857 MinRemainingCapacity : 1 ,
801858 },
802- Filter : & azdext.AiModelFilterOptions {
803- Locations : []string {azureContext .Scope .Location },
804- },
805859 DefaultValue : defaultModel ,
806860 }
807861
@@ -813,16 +867,15 @@ func selectNewModel(
813867 return modelResp .Model , nil
814868}
815869
816- // resolveModelDeployment resolves a model deployment without prompting, selecting the best
817- // candidate based on default versions, SKU priority, and available quota. Both init flows
818- // use this for the "deploy new model" path in non-interactive mode.
819- func resolveModelDeployment (
870+ // resolveModelDeployments resolves model deployments without prompting, returning all candidates
871+ // filtered by location and quota. Both init flows use this for deployment resolution.
872+ func resolveModelDeployments (
820873 ctx context.Context ,
821874 azdClient * azdext.AzdClient ,
822875 azureContext * azdext.AzureContext ,
823876 model * azdext.AiModel ,
824877 location string ,
825- ) (* azdext.AiModelDeployment , error ) {
878+ ) ([] * azdext.AiModelDeployment , error ) {
826879 resolveResp , err := azdClient .Ai ().ResolveModelDeployments (ctx , & azdext.ResolveModelDeploymentsRequest {
827880 AzureContext : azureContext ,
828881 ModelName : model .Name ,
@@ -834,19 +887,22 @@ func resolveModelDeployment(
834887 },
835888 })
836889 if err != nil {
837- return nil , exterrors . FromAiService ( err , exterrors . CodeModelResolutionFailed )
890+ return nil , err
838891 }
839892
840- if len (resolveResp .Deployments ) == 0 {
841- return nil , exterrors .Dependency (
842- exterrors .CodeModelResolutionFailed ,
843- fmt .Sprintf ("no deployment candidates found for model '%s' in location '%s'" , model .Name , location ),
844- "" ,
845- )
893+ return resolveResp .Deployments , nil
894+ }
895+
896+ func selectBestModelDeploymentCandidate (
897+ model * azdext.AiModel ,
898+ deployments []* azdext.AiModelDeployment ,
899+ ) * azdext.AiModelDeployment {
900+ if len (deployments ) == 0 {
901+ return nil
846902 }
847903
848- orderedCandidates := make ([]* azdext.AiModelDeployment , len (resolveResp . Deployments ))
849- copy (orderedCandidates , resolveResp . Deployments )
904+ orderedCandidates := make ([]* azdext.AiModelDeployment , len (deployments ))
905+ copy (orderedCandidates , deployments )
850906
851907 defaultVersions := make (map [string ]struct {}, len (model .Versions ))
852908 for _ , version := range model .Versions {
@@ -863,7 +919,34 @@ func resolveModelDeployment(
863919 continue
864920 }
865921
866- return cloneDeploymentWithCapacity (candidate , capacity ), nil
922+ return cloneDeploymentWithCapacity (candidate , capacity )
923+ }
924+
925+ return nil
926+ }
927+
928+ func resolveModelDeployment (
929+ ctx context.Context ,
930+ azdClient * azdext.AzdClient ,
931+ azureContext * azdext.AzureContext ,
932+ model * azdext.AiModel ,
933+ location string ,
934+ ) (* azdext.AiModelDeployment , error ) {
935+ deployments , err := resolveModelDeployments (ctx , azdClient , azureContext , model , location )
936+ if err != nil {
937+ return nil , exterrors .FromAiService (err , exterrors .CodeModelResolutionFailed )
938+ }
939+
940+ if len (deployments ) == 0 {
941+ return nil , exterrors .Dependency (
942+ exterrors .CodeModelResolutionFailed ,
943+ fmt .Sprintf ("no deployment candidates found for model '%s' in location '%s'" , model .Name , location ),
944+ "" ,
945+ )
946+ }
947+
948+ if candidate := selectBestModelDeploymentCandidate (model , deployments ); candidate != nil {
949+ return candidate , nil
867950 }
868951
869952 return nil , fmt .Errorf ("no deployment candidates found for model '%s' with a valid non-interactive capacity" , model .Name )
0 commit comments