From c284c96375763d0d1d494458a90064b9e7834ffa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 05:12:30 +0000 Subject: [PATCH 1/2] Add GPU instance search command with filtering and sorting Add a new `brev gpus` command that allows users to search and filter GPU instance types from the API. Features include: - Filter by GPU name (case-insensitive partial match) - Filter by minimum GPU memory per GPU - Filter by minimum total VRAM across all GPUs - Filter by minimum GPU compute capability - Filter by cloud provider - Filter to show only available instances - Sort by any column (instance-type, gpu-name, memory, price, etc.) - Sort in ascending or descending order --- pkg/cmd/cmd.go | 2 + pkg/cmd/gpusearch/gpusearch.go | 305 +++++++++++++++++++++++++++++++++ pkg/entity/entity.go | 17 ++ pkg/store/workspace.go | 32 ++++ 4 files changed, 356 insertions(+) create mode 100644 pkg/cmd/gpusearch/gpusearch.go diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 2980b0ce..04564f9c 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -14,6 +14,7 @@ import ( "github.com/brevdev/brev-cli/pkg/cmd/delete" "github.com/brevdev/brev-cli/pkg/cmd/envvars" "github.com/brevdev/brev-cli/pkg/cmd/fu" + "github.com/brevdev/brev-cli/pkg/cmd/gpusearch" "github.com/brevdev/brev-cli/pkg/cmd/healthcheck" "github.com/brevdev/brev-cli/pkg/cmd/hello" "github.com/brevdev/brev-cli/pkg/cmd/importideconfig" @@ -270,6 +271,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor } cmd.AddCommand(workspacegroups.NewCmdWorkspaceGroups(t, loginCmdStore)) cmd.AddCommand(scale.NewCmdScale(t, noLoginCmdStore)) + cmd.AddCommand(gpusearch.NewCmdGPUSearch(t, noLoginCmdStore)) cmd.AddCommand(configureenvvars.NewCmdConfigureEnvVars(t, loginCmdStore)) cmd.AddCommand(importideconfig.NewCmdImportIDEConfig(t, noLoginCmdStore)) cmd.AddCommand(shell.NewCmdShell(t, loginCmdStore, noLoginCmdStore)) diff --git a/pkg/cmd/gpusearch/gpusearch.go b/pkg/cmd/gpusearch/gpusearch.go new file mode 100644 index 00000000..76335846 --- /dev/null +++ b/pkg/cmd/gpusearch/gpusearch.go @@ -0,0 +1,305 @@ +// Package gpusearch provides a command to search and filter GPU instance types +package gpusearch + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/brevdev/brev-cli/pkg/entity" + breverrors "github.com/brevdev/brev-cli/pkg/errors" + "github.com/brevdev/brev-cli/pkg/terminal" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +var ( + long = `Search and filter GPU instance types available on Brev. + +Filter by GPU name, memory, capability, and more. Sort results by different columns.` + + example = ` + # List all GPU instances + brev gpus + + # Filter by GPU name (case-insensitive, partial match) + brev gpus --gpu-name a100 + brev gpus -n h100 + + # Filter by minimum GPU memory per GPU (in GB) + brev gpus --gpu-memory 40 + + # Filter by minimum total VRAM across all GPUs (in GB) + brev gpus --total-vram 80 + + # Filter by minimum GPU capability (compute capability number) + brev gpus --capability 8.0 + + # Filter by provider + brev gpus --provider aws + brev gpus --provider gcp + + # Combine filters + brev gpus --gpu-name a100 --gpu-memory 40 --provider aws + + # Sort by different columns (default: instance-type) + brev gpus --sort gpu-name + brev gpus --sort gpu-memory + brev gpus --sort total-vram + brev gpus --sort capability + brev gpus --sort price + brev gpus --sort gpu-count + + # Sort in descending order + brev gpus --sort price --desc + + # Show only available instances + brev gpus --available +` +) + +// GPUSearchStore defines the interface for fetching GPU instance types +type GPUSearchStore interface { + GetInstanceTypes() ([]entity.GPUInstanceType, error) +} + +// NewCmdGPUSearch creates the gpus command +func NewCmdGPUSearch(t *terminal.Terminal, store GPUSearchStore) *cobra.Command { + var gpuName string + var gpuMemory float64 + var totalVRAM float64 + var capability float64 + var provider string + var sortBy string + var descending bool + var availableOnly bool + + cmd := &cobra.Command{ + Annotations: map[string]string{"workspace": ""}, + Use: "gpus", + Aliases: []string{"gpu-search", "gpu"}, + DisableFlagsInUseLine: true, + Short: "Search and filter GPU instance types", + Long: long, + Example: example, + RunE: func(cmd *cobra.Command, args []string) error { + err := RunGPUSearch(t, store, GPUSearchOptions{ + GPUName: gpuName, + GPUMemory: gpuMemory, + TotalVRAM: totalVRAM, + Capability: capability, + Provider: provider, + SortBy: sortBy, + Descending: descending, + AvailableOnly: availableOnly, + }) + if err != nil { + return breverrors.WrapAndTrace(err) + } + return nil + }, + } + + cmd.Flags().StringVarP(&gpuName, "gpu-name", "n", "", "Filter by GPU name (case-insensitive, partial match)") + cmd.Flags().Float64VarP(&gpuMemory, "gpu-memory", "m", 0, "Filter by minimum GPU memory per GPU (in GB)") + cmd.Flags().Float64VarP(&totalVRAM, "total-vram", "v", 0, "Filter by minimum total VRAM across all GPUs (in GB)") + cmd.Flags().Float64VarP(&capability, "capability", "c", 0, "Filter by minimum GPU capability (compute capability number)") + cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter by cloud provider (e.g., aws, gcp)") + cmd.Flags().StringVarP(&sortBy, "sort", "s", "instance-type", "Sort by column: instance-type, gpu-name, gpu-memory, total-vram, capability, price, gpu-count") + cmd.Flags().BoolVarP(&descending, "desc", "d", false, "Sort in descending order") + cmd.Flags().BoolVarP(&availableOnly, "available", "a", false, "Show only available instances") + + return cmd +} + +// GPUSearchOptions contains the filter and sort options +type GPUSearchOptions struct { + GPUName string + GPUMemory float64 + TotalVRAM float64 + Capability float64 + Provider string + SortBy string + Descending bool + AvailableOnly bool +} + +// RunGPUSearch executes the GPU search with the given options +func RunGPUSearch(t *terminal.Terminal, store GPUSearchStore, opts GPUSearchOptions) error { + instances, err := store.GetInstanceTypes() + if err != nil { + return breverrors.WrapAndTrace(err) + } + + if len(instances) == 0 { + t.Vprint(t.Yellow("No GPU instance types found.")) + return nil + } + + // Apply filters + filtered := filterInstances(instances, opts) + + if len(filtered) == 0 { + t.Vprint(t.Yellow("No GPU instances match the specified filters.")) + return nil + } + + // Sort results + sortInstances(filtered, opts.SortBy, opts.Descending) + + // Display results + displayInstancesTable(t, filtered) + + t.Vprintf("\nFound %d GPU instance type(s)\n", len(filtered)) + + return nil +} + +// filterInstances applies all filters to the instance list +func filterInstances(instances []entity.GPUInstanceType, opts GPUSearchOptions) []entity.GPUInstanceType { + var filtered []entity.GPUInstanceType + + for _, inst := range instances { + // Filter by availability + if opts.AvailableOnly && !inst.Available { + continue + } + + // Filter by GPU name (case-insensitive, partial match) + if opts.GPUName != "" { + if !strings.Contains(strings.ToLower(inst.GPUName), strings.ToLower(opts.GPUName)) { + continue + } + } + + // Filter by minimum GPU memory per GPU + if opts.GPUMemory > 0 && inst.GPUMemoryGB < opts.GPUMemory { + continue + } + + // Filter by minimum total VRAM + if opts.TotalVRAM > 0 && inst.TotalGPUMemoryGB < opts.TotalVRAM { + continue + } + + // Filter by minimum GPU capability + if opts.Capability > 0 && inst.GPUCapability < opts.Capability { + continue + } + + // Filter by provider (case-insensitive) + if opts.Provider != "" { + if !strings.EqualFold(inst.Provider, opts.Provider) { + continue + } + } + + filtered = append(filtered, inst) + } + + return filtered +} + +// sortInstances sorts the instances by the specified column +func sortInstances(instances []entity.GPUInstanceType, sortBy string, descending bool) { + sort.Slice(instances, func(i, j int) bool { + var less bool + + switch strings.ToLower(sortBy) { + case "gpu-name", "gpuname", "name": + less = strings.ToLower(instances[i].GPUName) < strings.ToLower(instances[j].GPUName) + case "gpu-memory", "gpumemory", "memory": + less = instances[i].GPUMemoryGB < instances[j].GPUMemoryGB + case "total-vram", "totalvram", "vram": + less = instances[i].TotalGPUMemoryGB < instances[j].TotalGPUMemoryGB + case "capability", "cap": + less = instances[i].GPUCapability < instances[j].GPUCapability + case "price", "cost": + less = instances[i].PricePerHour < instances[j].PricePerHour + case "gpu-count", "gpucount", "count", "gpus": + less = instances[i].GPUCount < instances[j].GPUCount + case "provider": + less = strings.ToLower(instances[i].Provider) < strings.ToLower(instances[j].Provider) + case "vcpus", "cpu": + less = instances[i].VCPUs < instances[j].VCPUs + default: // instance-type + less = strings.ToLower(instances[i].InstanceType) < strings.ToLower(instances[j].InstanceType) + } + + if descending { + return !less + } + return less + }) +} + +// displayInstancesTable renders the GPU instances in a table format +func displayInstancesTable(t *terminal.Terminal, instances []entity.GPUInstanceType) { + ta := table.NewWriter() + ta.SetOutputMirror(os.Stdout) + ta.Style().Options = getBrevTableOptions() + + header := table.Row{"Instance Type", "GPU", "GPU Mem", "GPUs", "Total VRAM", "Capability", "vCPUs", "RAM", "$/hr", "Provider", "Available"} + ta.AppendHeader(header) + + for _, inst := range instances { + availableStr := formatAvailable(t, inst.Available) + row := table.Row{ + inst.InstanceType, + inst.GPUName, + formatMemory(inst.GPUMemoryGB), + inst.GPUCount, + formatMemory(inst.TotalGPUMemoryGB), + formatCapability(inst.GPUCapability), + inst.VCPUs, + formatMemory(inst.MemoryGB), + formatPrice(inst.PricePerHour), + inst.Provider, + availableStr, + } + ta.AppendRow(row) + } + + ta.Render() +} + +func getBrevTableOptions() table.Options { + options := table.OptionsDefault + options.DrawBorder = false + options.SeparateColumns = false + options.SeparateRows = false + options.SeparateHeader = false + return options +} + +func formatMemory(gb float64) string { + if gb == 0 { + return "-" + } + if gb == float64(int(gb)) { + return fmt.Sprintf("%dGB", int(gb)) + } + return fmt.Sprintf("%.1fGB", gb) +} + +func formatCapability(cap float64) string { + if cap == 0 { + return "-" + } + return fmt.Sprintf("%.1f", cap) +} + +func formatPrice(price float64) string { + if price == 0 { + return "-" + } + return fmt.Sprintf("$%.2f", price) +} + +func formatAvailable(t *terminal.Terminal, available bool) string { + if available { + return t.Green("Yes") + } + return t.Red("No") +} diff --git a/pkg/entity/entity.go b/pkg/entity/entity.go index 57ca29ab..b1a07a80 100644 --- a/pkg/entity/entity.go +++ b/pkg/entity/entity.go @@ -583,3 +583,20 @@ type ModifyWorkspaceRequest struct { ExecsV1 *ExecsV1 `json:"execsV1"` InstanceType string `json:"instanceType"` } + +// GPUInstanceType represents a GPU instance type from the API +type GPUInstanceType struct { + InstanceType string `json:"instanceType"` + GPUName string `json:"gpuName"` + GPUMemoryGB float64 `json:"gpuMemoryGB"` + GPUCount int `json:"gpuCount"` + TotalGPUMemoryGB float64 `json:"totalGpuMemoryGB"` + GPUCapability float64 `json:"gpuCapability"` + VCPUs int `json:"vcpus"` + MemoryGB float64 `json:"memoryGB"` + PricePerHour float64 `json:"pricePerHour"` + Provider string `json:"provider"` + Region string `json:"region"` + Available bool `json:"available"` + InterconnectBandwidth string `json:"interconnectBandwidth,omitempty"` +} diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index 5190d313..787930bb 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -691,6 +691,38 @@ var ( ollamaModelPath = fmt.Sprintf(ollamaModelPathPattern, fmt.Sprintf("{%s}", modelNameParamName), fmt.Sprintf("{%s}", tagNameParamName)) ) +// GetInstanceTypes fetches GPU instance types from the API +func (s NoAuthHTTPStore) GetInstanceTypes() ([]entity.GPUInstanceType, error) { + var result []entity.GPUInstanceType + res, err := s.noAuthHTTPClient.restyClient.R(). + SetHeader("Content-Type", "application/json"). + SetResult(&result). + Get("v1/instance/types") + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if res.IsError() { + return nil, NewHTTPResponseError(res) + } + return result, nil +} + +// GetInstanceTypes fetches GPU instance types from the API (authenticated version) +func (s AuthHTTPStore) GetInstanceTypes() ([]entity.GPUInstanceType, error) { + var result []entity.GPUInstanceType + res, err := s.authHTTPClient.restyClient.R(). + SetHeader("Content-Type", "application/json"). + SetResult(&result). + Get("v1/instance/types") + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if res.IsError() { + return nil, NewHTTPResponseError(res) + } + return result, nil +} + func ValidateOllamaModel(model string, tag string) (bool, error) { // Special case for deepseek models if strings.HasPrefix(model, "deepseek-r1") { From 99040b846c775780aa21eeedca5c27c43c895219 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 05:23:11 +0000 Subject: [PATCH 2/2] Fix API URL and add unit tests for GPU search - Use public api.brev.dev endpoint for instance types - Add comprehensive unit tests for filtering and sorting logic - Tests cover: GPU name, memory, VRAM, capability, provider filters - Tests cover: all sort columns and descending order --- pkg/cmd/gpusearch/gpusearch_test.go | 280 ++++++++++++++++++++++++++++ pkg/store/workspace.go | 15 +- 2 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/gpusearch/gpusearch_test.go diff --git a/pkg/cmd/gpusearch/gpusearch_test.go b/pkg/cmd/gpusearch/gpusearch_test.go new file mode 100644 index 00000000..3dda1204 --- /dev/null +++ b/pkg/cmd/gpusearch/gpusearch_test.go @@ -0,0 +1,280 @@ +package gpusearch + +import ( + "testing" + + "github.com/brevdev/brev-cli/pkg/entity" + "github.com/stretchr/testify/assert" +) + +// Mock data for testing +var testInstances = []entity.GPUInstanceType{ + { + InstanceType: "p4d.24xlarge", + GPUName: "NVIDIA A100", + GPUMemoryGB: 40, + GPUCount: 8, + TotalGPUMemoryGB: 320, + GPUCapability: 8.0, + VCPUs: 96, + MemoryGB: 1152, + PricePerHour: 32.77, + Provider: "aws", + Available: true, + }, + { + InstanceType: "p3.2xlarge", + GPUName: "NVIDIA V100", + GPUMemoryGB: 16, + GPUCount: 1, + TotalGPUMemoryGB: 16, + GPUCapability: 7.0, + VCPUs: 8, + MemoryGB: 61, + PricePerHour: 3.06, + Provider: "aws", + Available: true, + }, + { + InstanceType: "g5.xlarge", + GPUName: "NVIDIA A10G", + GPUMemoryGB: 24, + GPUCount: 1, + TotalGPUMemoryGB: 24, + GPUCapability: 8.6, + VCPUs: 4, + MemoryGB: 16, + PricePerHour: 1.01, + Provider: "aws", + Available: false, + }, + { + InstanceType: "a2-highgpu-1g", + GPUName: "NVIDIA A100", + GPUMemoryGB: 40, + GPUCount: 1, + TotalGPUMemoryGB: 40, + GPUCapability: 8.0, + VCPUs: 12, + MemoryGB: 85, + PricePerHour: 3.67, + Provider: "gcp", + Available: true, + }, + { + InstanceType: "h100-80gb-1", + GPUName: "NVIDIA H100", + GPUMemoryGB: 80, + GPUCount: 1, + TotalGPUMemoryGB: 80, + GPUCapability: 9.0, + VCPUs: 16, + MemoryGB: 128, + PricePerHour: 5.50, + Provider: "gcp", + Available: true, + }, +} + +func TestFilterByGPUName(t *testing.T) { + opts := GPUSearchOptions{GPUName: "a100"} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 2) + for _, inst := range filtered { + assert.Contains(t, inst.GPUName, "A100") + } +} + +func TestFilterByGPUNameCaseInsensitive(t *testing.T) { + opts := GPUSearchOptions{GPUName: "H100"} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 1) + assert.Equal(t, "NVIDIA H100", filtered[0].GPUName) +} + +func TestFilterByGPUMemory(t *testing.T) { + opts := GPUSearchOptions{GPUMemory: 40} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 3) + for _, inst := range filtered { + assert.GreaterOrEqual(t, inst.GPUMemoryGB, 40.0) + } +} + +func TestFilterByTotalVRAM(t *testing.T) { + opts := GPUSearchOptions{TotalVRAM: 80} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 2) + for _, inst := range filtered { + assert.GreaterOrEqual(t, inst.TotalGPUMemoryGB, 80.0) + } +} + +func TestFilterByCapability(t *testing.T) { + opts := GPUSearchOptions{Capability: 8.5} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 2) + for _, inst := range filtered { + assert.GreaterOrEqual(t, inst.GPUCapability, 8.5) + } +} + +func TestFilterByProvider(t *testing.T) { + opts := GPUSearchOptions{Provider: "gcp"} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 2) + for _, inst := range filtered { + assert.Equal(t, "gcp", inst.Provider) + } +} + +func TestFilterByProviderCaseInsensitive(t *testing.T) { + opts := GPUSearchOptions{Provider: "AWS"} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 3) + for _, inst := range filtered { + assert.Equal(t, "aws", inst.Provider) + } +} + +func TestFilterByAvailableOnly(t *testing.T) { + opts := GPUSearchOptions{AvailableOnly: true} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 4) + for _, inst := range filtered { + assert.True(t, inst.Available) + } +} + +func TestCombinedFilters(t *testing.T) { + opts := GPUSearchOptions{ + GPUName: "a100", + GPUMemory: 40, + Provider: "aws", + AvailableOnly: true, + } + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 1) + assert.Equal(t, "p4d.24xlarge", filtered[0].InstanceType) +} + +func TestSortByInstanceType(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "instance-type", false) + + assert.Equal(t, "a2-highgpu-1g", instances[0].InstanceType) + assert.Equal(t, "p4d.24xlarge", instances[len(instances)-1].InstanceType) +} + +func TestSortByGPUName(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "gpu-name", false) + + assert.Equal(t, "NVIDIA A100", instances[0].GPUName) + assert.Equal(t, "NVIDIA V100", instances[len(instances)-1].GPUName) +} + +func TestSortByPrice(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "price", false) + + assert.Equal(t, 1.01, instances[0].PricePerHour) + assert.Equal(t, 32.77, instances[len(instances)-1].PricePerHour) +} + +func TestSortByPriceDescending(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "price", true) + + assert.Equal(t, 32.77, instances[0].PricePerHour) + assert.Equal(t, 1.01, instances[len(instances)-1].PricePerHour) +} + +func TestSortByGPUMemory(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "gpu-memory", false) + + assert.Equal(t, 16.0, instances[0].GPUMemoryGB) + assert.Equal(t, 80.0, instances[len(instances)-1].GPUMemoryGB) +} + +func TestSortByTotalVRAM(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "total-vram", false) + + assert.Equal(t, 16.0, instances[0].TotalGPUMemoryGB) + assert.Equal(t, 320.0, instances[len(instances)-1].TotalGPUMemoryGB) +} + +func TestSortByCapability(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "capability", false) + + assert.Equal(t, 7.0, instances[0].GPUCapability) + assert.Equal(t, 9.0, instances[len(instances)-1].GPUCapability) +} + +func TestSortByGPUCount(t *testing.T) { + instances := make([]entity.GPUInstanceType, len(testInstances)) + copy(instances, testInstances) + + sortInstances(instances, "gpu-count", false) + + assert.Equal(t, 1, instances[0].GPUCount) + assert.Equal(t, 8, instances[len(instances)-1].GPUCount) +} + +func TestFormatMemory(t *testing.T) { + assert.Equal(t, "40GB", formatMemory(40)) + assert.Equal(t, "16.5GB", formatMemory(16.5)) + assert.Equal(t, "-", formatMemory(0)) +} + +func TestFormatCapability(t *testing.T) { + assert.Equal(t, "8.0", formatCapability(8.0)) + assert.Equal(t, "7.5", formatCapability(7.5)) + assert.Equal(t, "-", formatCapability(0)) +} + +func TestFormatPrice(t *testing.T) { + assert.Equal(t, "$3.06", formatPrice(3.06)) + assert.Equal(t, "$32.77", formatPrice(32.77)) + assert.Equal(t, "-", formatPrice(0)) +} + +func TestEmptyFilter(t *testing.T) { + opts := GPUSearchOptions{} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, len(testInstances)) +} + +func TestNoMatchingFilter(t *testing.T) { + opts := GPUSearchOptions{GPUName: "nonexistent"} + filtered := filterInstances(testInstances, opts) + + assert.Len(t, filtered, 0) +} diff --git a/pkg/store/workspace.go b/pkg/store/workspace.go index 787930bb..0b4fc08d 100644 --- a/pkg/store/workspace.go +++ b/pkg/store/workspace.go @@ -691,13 +691,18 @@ var ( ollamaModelPath = fmt.Sprintf(ollamaModelPathPattern, fmt.Sprintf("{%s}", modelNameParamName), fmt.Sprintf("{%s}", tagNameParamName)) ) +// instanceTypesAPIURL is the public API endpoint for instance types +const instanceTypesAPIURL = "https://api.brev.dev" + // GetInstanceTypes fetches GPU instance types from the API func (s NoAuthHTTPStore) GetInstanceTypes() ([]entity.GPUInstanceType, error) { var result []entity.GPUInstanceType - res, err := s.noAuthHTTPClient.restyClient.R(). + // Use a separate client for the public API endpoint + client := resty.New().SetBaseURL(instanceTypesAPIURL) + res, err := client.R(). SetHeader("Content-Type", "application/json"). SetResult(&result). - Get("v1/instance/types") + Get("/v1/instance/types") if err != nil { return nil, breverrors.WrapAndTrace(err) } @@ -710,10 +715,12 @@ func (s NoAuthHTTPStore) GetInstanceTypes() ([]entity.GPUInstanceType, error) { // GetInstanceTypes fetches GPU instance types from the API (authenticated version) func (s AuthHTTPStore) GetInstanceTypes() ([]entity.GPUInstanceType, error) { var result []entity.GPUInstanceType - res, err := s.authHTTPClient.restyClient.R(). + // Use a separate client for the public API endpoint + client := resty.New().SetBaseURL(instanceTypesAPIURL) + res, err := client.R(). SetHeader("Content-Type", "application/json"). SetResult(&result). - Get("v1/instance/types") + Get("/v1/instance/types") if err != nil { return nil, breverrors.WrapAndTrace(err) }