@@ -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"
@@ -722,23 +723,27 @@ func ensureLocation(
722723 azureContext * azdext.AzureContext ,
723724 envName string ,
724725) error {
725- if azureContext .Scope .Location != "" {
726+ allowedLocations := supportedRegionsForInit ()
727+
728+ if azureContext .Scope .Location != "" && locationAllowed (azureContext .Scope .Location , allowedLocations ) {
726729 return nil
727730 }
731+ if azureContext .Scope .Location != "" {
732+ fmt .Printf ("%s" , output .WithWarningFormat (
733+ "The current AZURE_LOCATION '%s' is not supported for this agent setup. Please choose a different location.\n " ,
734+ azureContext .Scope .Location ,
735+ ))
736+ azureContext .Scope .Location = ""
737+ }
728738
729739 fmt .Println ("Select an Azure location. This determines which models are available and where your Foundry project resources will be deployed." )
730740
731- locationResponse , err := azdClient .Prompt ().PromptLocation (ctx , & azdext.PromptLocationRequest {
732- AzureContext : azureContext ,
733- })
741+ locationName , err := promptLocationForInit (ctx , azdClient , azureContext , allowedLocations )
734742 if err != nil {
735- if exterrors .IsCancellation (err ) {
736- return exterrors .Cancelled ("location selection was cancelled" )
737- }
738- return exterrors .FromPrompt (err , "failed to prompt for location" )
743+ return err
739744 }
740745
741- azureContext .Scope .Location = locationResponse . Location . Name
746+ azureContext .Scope .Location = locationName
742747
743748 return setEnvValue (ctx , azdClient , envName , "AZURE_LOCATION" , azureContext .Scope .Location )
744749}
@@ -764,6 +769,61 @@ func ensureSubscriptionAndLocation(
764769 return newCredential , nil
765770}
766771
772+ func normalizeLocationName (location string ) string {
773+ return strings .TrimSpace (strings .ToLower (location ))
774+ }
775+
776+ func locationAllowed (location string , allowedLocations []string ) bool {
777+ if len (allowedLocations ) == 0 {
778+ return true
779+ }
780+
781+ normalized := normalizeLocationName (location )
782+ for _ , allowed := range allowedLocations {
783+ if normalized == normalizeLocationName (allowed ) {
784+ return true
785+ }
786+ }
787+
788+ return false
789+ }
790+
791+ func promptLocationForInit (
792+ ctx context.Context ,
793+ azdClient * azdext.AzdClient ,
794+ azureContext * azdext.AzureContext ,
795+ allowedLocations []string ,
796+ ) (string , error ) {
797+ locationResponse , err := azdClient .Prompt ().PromptLocation (ctx , & azdext.PromptLocationRequest {
798+ AzureContext : azureContext ,
799+ AllowedLocations : allowedLocations ,
800+ })
801+ if err != nil {
802+ if exterrors .IsCancellation (err ) {
803+ return "" , exterrors .Cancelled ("location selection was cancelled" )
804+ }
805+ return "" , exterrors .FromPrompt (err , "failed to prompt for location" )
806+ }
807+
808+ return locationResponse .Location .Name , nil
809+ }
810+
811+ func agentModelFilter (locations []string , excludeModelNames []string ) * azdext.AiModelFilterOptions {
812+ filter := & azdext.AiModelFilterOptions {
813+ Capabilities : []string {agentsV2ModelCapability },
814+ }
815+
816+ if len (locations ) > 0 {
817+ filter .Locations = locations
818+ }
819+
820+ if len (excludeModelNames ) > 0 {
821+ filter .ExcludeModelNames = excludeModelNames
822+ }
823+
824+ return filter
825+ }
826+
767827// --- Shared model helpers ---
768828
769829// selectNewModel prompts the user to select a model from the AI catalog, filtered by location.
@@ -781,15 +841,13 @@ func selectNewModel(
781841
782842 promptReq := & azdext.PromptAiModelRequest {
783843 AzureContext : azureContext ,
844+ Filter : agentModelFilter ([]string {azureContext .Scope .Location }, nil ),
784845 SelectOptions : & azdext.SelectOptions {
785846 Message : "Select a model" ,
786847 },
787848 Quota : & azdext.QuotaCheckOptions {
788849 MinRemainingCapacity : 1 ,
789850 },
790- Filter : & azdext.AiModelFilterOptions {
791- Locations : []string {azureContext .Scope .Location },
792- },
793851 DefaultValue : defaultModel ,
794852 }
795853
@@ -804,13 +862,13 @@ func selectNewModel(
804862// resolveModelDeployment resolves a model deployment without prompting, selecting the best
805863// candidate based on default versions, SKU priority, and available quota. Both init flows
806864// use this for the "deploy new model" path in non-interactive mode.
807- func resolveModelDeployment (
865+ func resolveModelDeployments (
808866 ctx context.Context ,
809867 azdClient * azdext.AzdClient ,
810868 azureContext * azdext.AzureContext ,
811869 model * azdext.AiModel ,
812870 location string ,
813- ) (* azdext.AiModelDeployment , error ) {
871+ ) ([] * azdext.AiModelDeployment , error ) {
814872 resolveResp , err := azdClient .Ai ().ResolveModelDeployments (ctx , & azdext.ResolveModelDeploymentsRequest {
815873 AzureContext : azureContext ,
816874 ModelName : model .Name ,
@@ -822,19 +880,22 @@ func resolveModelDeployment(
822880 },
823881 })
824882 if err != nil {
825- return nil , exterrors . FromAiService ( err , exterrors . CodeModelResolutionFailed )
883+ return nil , err
826884 }
827885
828- if len (resolveResp .Deployments ) == 0 {
829- return nil , exterrors .Dependency (
830- exterrors .CodeModelResolutionFailed ,
831- fmt .Sprintf ("no deployment candidates found for model '%s' in location '%s'" , model .Name , location ),
832- "" ,
833- )
886+ return resolveResp .Deployments , nil
887+ }
888+
889+ func selectBestModelDeploymentCandidate (
890+ model * azdext.AiModel ,
891+ deployments []* azdext.AiModelDeployment ,
892+ ) * azdext.AiModelDeployment {
893+ if len (deployments ) == 0 {
894+ return nil
834895 }
835896
836- orderedCandidates := make ([]* azdext.AiModelDeployment , len (resolveResp . Deployments ))
837- copy (orderedCandidates , resolveResp . Deployments )
897+ orderedCandidates := make ([]* azdext.AiModelDeployment , len (deployments ))
898+ copy (orderedCandidates , deployments )
838899
839900 defaultVersions := make (map [string ]struct {}, len (model .Versions ))
840901 for _ , version := range model .Versions {
@@ -851,7 +912,34 @@ func resolveModelDeployment(
851912 continue
852913 }
853914
854- return cloneDeploymentWithCapacity (candidate , capacity ), nil
915+ return cloneDeploymentWithCapacity (candidate , capacity )
916+ }
917+
918+ return nil
919+ }
920+
921+ func resolveModelDeployment (
922+ ctx context.Context ,
923+ azdClient * azdext.AzdClient ,
924+ azureContext * azdext.AzureContext ,
925+ model * azdext.AiModel ,
926+ location string ,
927+ ) (* azdext.AiModelDeployment , error ) {
928+ deployments , err := resolveModelDeployments (ctx , azdClient , azureContext , model , location )
929+ if err != nil {
930+ return nil , exterrors .FromAiService (err , exterrors .CodeModelResolutionFailed )
931+ }
932+
933+ if len (deployments ) == 0 {
934+ return nil , exterrors .Dependency (
935+ exterrors .CodeModelResolutionFailed ,
936+ fmt .Sprintf ("no deployment candidates found for model '%s' in location '%s'" , model .Name , location ),
937+ "" ,
938+ )
939+ }
940+
941+ if candidate := selectBestModelDeploymentCandidate (model , deployments ); candidate != nil {
942+ return candidate , nil
855943 }
856944
857945 return nil , fmt .Errorf ("no deployment candidates found for model '%s' with a valid non-interactive capacity" , model .Name )
0 commit comments