@@ -181,6 +181,12 @@ func NewCmdGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore) *cobra
181181 name = args [0 ]
182182 }
183183
184+ // Normalize --region: provider region names are case-sensitive on the
185+ // server side (e.g. GCP rejects "US-WEST1", accepts "us-west1"), while
186+ // our client-side validation is case-insensitive. Lowercase here so
187+ // what we validate matches what we send.
188+ filters .region = strings .ToLower (strings .TrimSpace (filters .region ))
189+
184190 launchableID , err := parseLaunchableID (launchable )
185191 if err != nil {
186192 return err
@@ -237,25 +243,41 @@ func NewCmdGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore) *cobra
237243 Region : filters .region ,
238244 }
239245
246+ // Fetch the instance-type catalog at most once per invocation. Region
247+ // validation, auto-search, and dry-run preview all share the same response
248+ // so we never issue more than one round-trip. --type explicit + no region
249+ // skips the fetch entirely (runDryRun returns early on explicit specs).
250+ var catalogItems []gpusearch.InstanceType
251+ needCatalog := filters .region != "" || (len (types ) == 0 && launchableID == "" )
252+ if needCatalog {
253+ resp , err := gpuCreateStore .GetInstanceTypes (false )
254+ if err != nil {
255+ return breverrors .WrapAndTrace (err )
256+ }
257+ if resp != nil {
258+ catalogItems = resp .Items
259+ }
260+ }
261+
240262 if filters .region != "" {
241- if err := validateRegionExists (filters .region , gpuCreateStore ); err != nil {
263+ if err := validateRegionExists (filters .region , catalogItems ); err != nil {
242264 return err
243265 }
244266 }
245267
246- opts .InstanceTypes , err = resolveInstanceTypes (cmd , gpuCreateStore , opts , types , & filters )
268+ opts .InstanceTypes , err = resolveInstanceTypes (cmd , catalogItems , opts , types , & filters )
247269 if err != nil {
248270 return err
249271 }
250272
251273 if filters .region != "" {
252- if err := validateRegion (filters .region , opts .InstanceTypes , gpuCreateStore ); err != nil {
274+ if err := validateRegion (filters .region , opts .InstanceTypes , catalogItems ); err != nil {
253275 return err
254276 }
255277 }
256278
257279 if dryRun {
258- return runDryRun (t , gpuCreateStore , opts .InstanceTypes , & filters )
280+ return runDryRun (t , catalogItems , opts .InstanceTypes , & filters )
259281 }
260282
261283 return RunGPUCreate (t , gpuCreateStore , opts )
@@ -463,8 +485,9 @@ func launchableBuildModeName(info *store.LaunchableResponse) string {
463485 }
464486}
465487
466- // resolveInstanceTypes determines instance types from launchable, flags, or filters
467- func resolveInstanceTypes (cmd * cobra.Command , gpuCreateStore GPUCreateStore , opts GPUCreateOptions , types []InstanceSpec , filters * searchFilterFlags ) ([]InstanceSpec , error ) {
488+ // resolveInstanceTypes determines instance types from launchable, flags, or filters.
489+ // Operates on a pre-fetched catalog when auto-search is needed.
490+ func resolveInstanceTypes (cmd * cobra.Command , items []gpusearch.InstanceType , opts GPUCreateOptions , types []InstanceSpec , filters * searchFilterFlags ) ([]InstanceSpec , error ) {
468491 if opts .LaunchableID != "" && len (types ) == 0 && ! cmd .Flags ().Changed ("type" ) {
469492 instanceType := ""
470493 if opts .LaunchableInfo != nil {
@@ -477,10 +500,7 @@ func resolveInstanceTypes(cmd *cobra.Command, gpuCreateStore GPUCreateStore, opt
477500 }
478501
479502 if len (types ) == 0 {
480- filtered , err := getFilteredInstanceTypes (gpuCreateStore , filters )
481- if err != nil {
482- return nil , breverrors .WrapAndTrace (err )
483- }
503+ filtered := getFilteredInstanceTypes (items , filters )
484504 if len (filtered ) == 0 {
485505 return nil , breverrors .NewValidationError ("no GPU instances match the specified filters. Try 'brev search' to see available options" )
486506 }
@@ -511,15 +531,11 @@ func parseStartupScript(value string) (string, error) {
511531 return value , nil
512532}
513533
514- // searchInstances fetches and filters GPU instances using user-provided filters merged with defaults
515- func searchInstances (s GPUCreateStore , filters * searchFilterFlags ) ([]gpusearch.GPUInstanceInfo , float64 , error ) {
516- response , err := s .GetInstanceTypes (false )
517- if err != nil {
518- return nil , 0 , breverrors .WrapAndTrace (err )
519- }
520-
521- if response == nil || len (response .Items ) == 0 {
522- return nil , 0 , nil
534+ // searchInstances filters GPU instances using user-provided filters merged with defaults.
535+ // Operates on a pre-fetched catalog so callers can share one network round-trip.
536+ func searchInstances (items []gpusearch.InstanceType , filters * searchFilterFlags ) ([]gpusearch.GPUInstanceInfo , float64 ) {
537+ if len (items ) == 0 {
538+ return nil , 0
523539 }
524540
525541 minTotalVRAM := orDefault (filters .minTotalVRAM , defaultMinTotalVRAM )
@@ -534,21 +550,19 @@ func searchInstances(s GPUCreateStore, filters *searchFilterFlags) ([]gpusearch.
534550 sortBy = "price"
535551 }
536552
537- instances := gpusearch .ProcessInstances (response . Items )
553+ instances := gpusearch .ProcessInstances (items )
538554 filtered := gpusearch .FilterInstances (instances , filters .gpuName , filters .region , filters .provider , "" , filters .minVRAM ,
539555 minTotalVRAM , minCapability , 0 , minDisk , 0 , maxBootTime , filters .stoppable , filters .rebootable , filters .flexPorts , true )
540556 gpusearch .SortInstances (filtered , sortBy , filters .descending )
541557
542- return filtered , minDisk , nil
558+ return filtered , minDisk
543559}
544560
545- // getFilteredInstanceTypes fetches GPU instance types using user-provided filters
561+ // getFilteredInstanceTypes filters GPU instance types using user-provided filters
546562// merged with defaults. When a filter flag is not set, the default value is used.
547- func getFilteredInstanceTypes (s GPUCreateStore , filters * searchFilterFlags ) ([]InstanceSpec , error ) {
548- filtered , minDisk , err := searchInstances (s , filters )
549- if err != nil {
550- return nil , breverrors .WrapAndTrace (err )
551- }
563+ // Operates on a pre-fetched catalog.
564+ func getFilteredInstanceTypes (items []gpusearch.InstanceType , filters * searchFilterFlags ) []InstanceSpec {
565+ filtered , minDisk := searchInstances (items , filters )
552566
553567 var specs []InstanceSpec
554568 for _ , inst := range filtered {
@@ -559,20 +573,18 @@ func getFilteredInstanceTypes(s GPUCreateStore, filters *searchFilterFlags) ([]I
559573 specs = append (specs , InstanceSpec {Type : inst .Type , DiskGB : diskGB })
560574 }
561575
562- return specs , nil
576+ return specs
563577}
564578
565- // runDryRun shows the instance types that would be used without creating anything
566- func runDryRun (t * terminal.Terminal , s GPUCreateStore , specs []InstanceSpec , filters * searchFilterFlags ) error {
579+ // runDryRun shows the instance types that would be used without creating anything.
580+ // Operates on a pre-fetched catalog.
581+ func runDryRun (t * terminal.Terminal , items []gpusearch.InstanceType , specs []InstanceSpec , filters * searchFilterFlags ) error {
567582 if len (specs ) > 0 {
568583 t .Print (formatInstanceSpecs (specs ))
569584 return nil
570585 }
571586
572- filtered , _ , err := searchInstances (s , filters )
573- if err != nil {
574- return breverrors .WrapAndTrace (err )
575- }
587+ filtered , _ := searchInstances (items , filters )
576588
577589 piped := gpusearch .IsStdoutPiped ()
578590 if err := gpusearch .DisplayGPUResults (t , filtered , false , piped , false ); err != nil {
@@ -582,18 +594,14 @@ func runDryRun(t *terminal.Terminal, s GPUCreateStore, specs []InstanceSpec, fil
582594}
583595
584596// validateRegionExists checks that the given region appears in at least one instance type in the catalog.
585- func validateRegionExists (region string , store GPUCreateStore ) error {
586- response , err := store .GetInstanceTypes (false )
587- if err != nil {
588- return breverrors .WrapAndTrace (err )
589- }
590- if response == nil || len (response .Items ) == 0 {
597+ // Operates on a pre-fetched catalog.
598+ func validateRegionExists (region string , items []gpusearch.InstanceType ) error {
599+ if len (items ) == 0 {
591600 return nil
592601 }
593602
594- regionLower := strings .ToLower (region )
595- for _ , item := range response .Items {
596- if typeSupportsRegion (item , regionLower ) {
603+ for _ , item := range items {
604+ if typeSupportsRegion (item , region ) {
597605 return nil
598606 }
599607 }
@@ -605,25 +613,17 @@ func validateRegionExists(region string, store GPUCreateStore) error {
605613}
606614
607615// validateRegion checks that every requested instance type is available in the given region.
608- func validateRegion (region string , types []InstanceSpec , store GPUCreateStore ) error {
609- if len (types ) == 0 {
610- return nil
611- }
612-
613- response , err := store .GetInstanceTypes (false )
614- if err != nil {
615- return breverrors .WrapAndTrace (err )
616- }
617- if response == nil || len (response .Items ) == 0 {
616+ // Operates on a pre-fetched catalog.
617+ func validateRegion (region string , types []InstanceSpec , items []gpusearch.InstanceType ) error {
618+ if len (types ) == 0 || len (items ) == 0 {
618619 return nil
619620 }
620621
621- catalog := make (map [string ]gpusearch.InstanceType , len (response . Items ))
622- for _ , item := range response . Items {
622+ catalog := make (map [string ]gpusearch.InstanceType , len (items ))
623+ for _ , item := range items {
623624 catalog [item .Type ] = item
624625 }
625626
626- regionLower := strings .ToLower (region )
627627 var unsupported []string
628628 var unknown []string
629629
@@ -633,7 +633,7 @@ func validateRegion(region string, types []InstanceSpec, store GPUCreateStore) e
633633 unknown = append (unknown , spec .Type )
634634 continue
635635 }
636- if ! typeSupportsRegion (item , regionLower ) {
636+ if ! typeSupportsRegion (item , region ) {
637637 unsupported = append (unsupported , spec .Type )
638638 }
639639 }
@@ -653,10 +653,12 @@ func validateRegion(region string, types []InstanceSpec, store GPUCreateStore) e
653653}
654654
655655// typeSupportsRegion reports whether an instance type lists the given region
656- // (already lowercased) in its AvailableLocations, using substring matching.
657- func typeSupportsRegion (item gpusearch.InstanceType , regionLower string ) bool {
656+ // in its AvailableLocations. Match is case-insensitive but must be an exact
657+ // equality — substring matching would let e.g. --region "us" pass validation
658+ // against "us-east-1", and then we'd forward "us" to the server as the region.
659+ func typeSupportsRegion (item gpusearch.InstanceType , region string ) bool {
658660 for _ , loc := range item .AvailableLocations {
659- if strings .Contains ( strings . ToLower ( loc ), regionLower ) {
661+ if strings .EqualFold ( loc , region ) {
660662 return true
661663 }
662664 }
0 commit comments