diff --git a/api/client.go b/api/client.go index a4430f8..9713daf 100644 --- a/api/client.go +++ b/api/client.go @@ -1,27 +1,37 @@ package api -import "net/http" +import ( + "bytes" + "encoding/json" + "io" + "net/http" + + "go.uber.org/zap" +) // Client handles API communication with the Vast.ai API type Client struct { apiKey string baseURL string httpClient *http.Client + logger *zap.SugaredLogger } // Error represents an API error response type APIError struct { - Success bool `json:"success"` - Error string `json:"error"` - Msg string `json:"msg"` + Success bool `json:"success"` } // NewClient creates a new Vast.ai API client func NewClient(apiKey string) *Client { + logger, _ := zap.NewDevelopment() + sugar := logger.Sugar() + return &Client{ apiKey: apiKey, baseURL: "https://console.vast.ai/api/v0", httpClient: &http.Client{}, + logger: sugar, } } @@ -32,11 +42,68 @@ func (c *Client) Apply(req *http.Request) { // makeRequest creates and sends an HTTP request with the provided method, path and body func (c *Client) makeRequest(method, path string, body interface{}) (*http.Response, error) { - req, err := http.NewRequest(method, c.baseURL+path, nil) + c.logger.Debugw("making request", + "method", method, + "path", path, + "body", body, + ) + + var bodyReader io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewBuffer(jsonBody) + } + + req, err := http.NewRequest(method, c.baseURL+path, bodyReader) if err != nil { + c.logger.Errorw("failed to create request", + "error", err, + "method", method, + "path", path, + ) return nil, err } c.Apply(req) - return c.httpClient.Do(req) + res, err := c.httpClient.Do(req) + + if err != nil { + c.logger.Errorw("failed to send request", + "error", err, + "method", method, + "path", path, + ) + return nil, err + } + + // debug print response + res_body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if res.StatusCode != http.StatusOK { + var apiError APIError + if err := json.NewDecoder(bytes.NewBuffer(res_body)).Decode(&apiError); err != nil { + c.logger.Errorw("failed to decode error response", + "error", err, + ) + } else { + c.logger.Errorw("api error", + "error", apiError, + ) + } + } else { + c.logger.Debugw("response", + "status", res.Status, + "status_code", res.StatusCode, + ) + } + + res.Body = io.NopCloser(bytes.NewBuffer(res_body)) + + return res, nil } diff --git a/api/search.go b/api/search.go index 6b02303..48b4bf0 100644 --- a/api/search.go +++ b/api/search.go @@ -5,22 +5,6 @@ import ( "net/http" ) -type ComparableInteger struct { - eq *int `json:"eq,omitempty"` - lt *int `json:"lt,omitempty"` - le *int `json:"le,omitempty"` - gt *int `json:"gt,omitempty"` - ge *int `json:"ge,omitempty"` -} - -type ComparableFloat struct { - eq *float64 `json:"eq,omitempty"` - lt *float64 `json:"lt,omitempty"` - le *float64 `json:"le,omitempty"` - gt *float64 `json:"gt,omitempty"` - ge *float64 `json:"ge,omitempty"` -} - type AdvancedSearch struct { Verified *bool `json:"verified,omitempty"` ComputeCap *ComparableInteger `json:"compute_cap,omitempty"` @@ -69,75 +53,12 @@ func NewSearch() *AdvancedSearch { } } -type Offer struct { - IsBid bool `json:"is_bid"` - InetUpBilled *float64 `json:"inet_up_billed"` - InetDownBilled *float64 `json:"inet_down_billed"` - External bool `json:"external"` - Webpage *string `json:"webpage"` - Logo string `json:"logo"` - Rentable bool `json:"rentable"` - ComputeCap int `json:"compute_cap"` - DriverVersion string `json:"driver_version"` - CudaMaxGood int `json:"cuda_max_good"` - MachineID int `json:"machine_id"` - HostingType *string `json:"hosting_type"` - PublicIPAddr string `json:"public_ipaddr"` - Geolocation string `json:"geolocation"` - FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"` - DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"` - Reliability2 float64 `json:"reliability2"` - HostRunTime int `json:"host_run_time"` - HostID int `json:"host_id"` - ID int `json:"id"` - BundleID int `json:"bundle_id"` - NumGPUs int `json:"num_gpus"` - TotalFlops float64 `json:"total_flops"` - MinBid float64 `json:"min_bid"` - DPHBase float64 `json:"dph_base"` - DPHTotal float64 `json:"dph_total"` - GPUName string `json:"gpu_name"` - GPURam int `json:"gpu_ram"` - GPUDisplayActive bool `json:"gpu_display_active"` - GPUMemBw float64 `json:"gpu_mem_bw"` - BwNVLink int `json:"bw_nvlink"` - DirectPortCount int `json:"direct_port_count"` - GPULanes int `json:"gpu_lanes"` - PCIeBw float64 `json:"pcie_bw"` - PCIGen int `json:"pci_gen"` - DLPerf float64 `json:"dlperf"` - CPUName string `json:"cpu_name"` - MoboName string `json:"mobo_name"` - CPURam int `json:"cpu_ram"` - CPUCores int `json:"cpu_cores"` - CPUCoresEffective int `json:"cpu_cores_effective"` - GPUFrac float64 `json:"gpu_frac"` - HasAVX int `json:"has_avx"` - DiskSpace float64 `json:"disk_space"` - DiskName string `json:"disk_name"` - DiskBw float64 `json:"disk_bw"` - InetUp float64 `json:"inet_up"` - InetDown float64 `json:"inet_down"` - StartDate float64 `json:"start_date"` - EndDate *float64 `json:"end_date"` - Duration *float64 `json:"duration"` - StorageCost float64 `json:"storage_cost"` - InetUpCost float64 `json:"inet_up_cost"` - InetDownCost float64 `json:"inet_down_cost"` - StorageTotalCost float64 `json:"storage_total_cost"` - Verification string `json:"verification"` - Score float64 `json:"score"` - Rented bool `json:"rented"` - BundledResults int `json:"bundled_results"` - PendingCount int `json:"pending_count"` -} - type SearchResponse struct { Offers []Offer `json:"offers"` } func (c *Client) Search(search *AdvancedSearch) (*SearchResponse, error) { - resp, err := c.makeRequest(http.MethodPost, "/bundles/", nil) + resp, err := c.makeRequest(http.MethodPost, "/bundles/", search) if err != nil { return nil, err } diff --git a/api/types.go b/api/types.go index 1c7b7fe..807b635 100644 --- a/api/types.go +++ b/api/types.go @@ -1,5 +1,7 @@ package api +import "fmt" + type PortMapping struct { HostIp string `json:"HostIp"` HostPort string `json:"HostPort"` @@ -42,7 +44,7 @@ type Instance struct { GPURam int `json:"gpu_ram"` GPUDisplayActive bool `json:"gpu_display_active"` GPUMemBW float64 `json:"gpu_mem_bw"` - BWNVLink int `json:"bw_nvlink"` + BWNVLink float64 `json:"bw_nvlink"` DirectPortCount int `json:"direct_port_count"` GPULanes int `json:"gpu_lanes"` PCIeBW float64 `json:"pcie_bw"` @@ -108,11 +110,12 @@ type Offer struct { Rentable bool `json:"rentable"` ComputeCap int `json:"compute_cap"` DriverVersion string `json:"driver_version"` - CudaMaxGood int `json:"cuda_max_good"` + CudaMaxGood float64 `json:"cuda_max_good"` MachineID int `json:"machine_id"` - HostingType *string `json:"hosting_type"` + HostingType *float64 `json:"hosting_type"` PublicIPAddr string `json:"public_ipaddr"` Geolocation string `json:"geolocation"` + Geocode *int64 `json:"geolocode"` FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"` DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"` Reliability2 float64 `json:"reliability2"` @@ -129,17 +132,17 @@ type Offer struct { GPURam int `json:"gpu_ram"` GPUDisplayActive bool `json:"gpu_display_active"` GPUMemBw float64 `json:"gpu_mem_bw"` - BwNVLink int `json:"bw_nvlink"` + BwNVLink float64 `json:"bw_nvlink"` DirectPortCount int `json:"direct_port_count"` GPULanes int `json:"gpu_lanes"` PCIeBw float64 `json:"pcie_bw"` - PCIGen int `json:"pci_gen"` + PCIGen float64 `json:"pci_gen"` DLPerf float64 `json:"dlperf"` CPUName string `json:"cpu_name"` MoboName string `json:"mobo_name"` CPURam int `json:"cpu_ram"` - CPUCores int `json:"cpu_cores"` - CPUCoresEffective int `json:"cpu_cores_effective"` + CPUCores float64 `json:"cpu_cores"` + CPUCoresEffective float64 `json:"cpu_cores_effective"` GPUFrac float64 `json:"gpu_frac"` HasAVX int `json:"has_avx"` DiskSpace float64 `json:"disk_space"` @@ -160,3 +163,11 @@ type Offer struct { BundledResults int `json:"bundled_results"` PendingCount int `json:"pending_count"` } + +func (o *Offer) String() string { + geocode := 0 + if o.Geocode != nil { + geocode = int(*o.Geocode) + } + return fmt.Sprintf("[%s] %s %s %d", o.GPUName, o.PublicIPAddr, o.Geolocation, geocode) +} diff --git a/api/utilities.go b/api/utilities.go index 5b0616e..55bc661 100644 --- a/api/utilities.go +++ b/api/utilities.go @@ -3,3 +3,19 @@ package api func Pointer[T any](d T) *T { return &d } + +type Comparable[T comparable] struct { + Eq *T `json:"eq,omitempty"` + Lt *T `json:"lt,omitempty"` + Le *T `json:"le,omitempty"` + Gt *T `json:"gt,omitempty"` + Ge *T `json:"ge,omitempty"` +} + +// Then you can use these type aliases for convenience +type ComparableInteger = Comparable[int] +type ComparableFloat = Comparable[float64] + +func Ge[T comparable](d T) *Comparable[T] { + return &Comparable[T]{Ge: Pointer(d)} +} diff --git a/cmd/quickvast/main.go b/cmd/quickvast/main.go index 2bee063..9751bfe 100644 --- a/cmd/quickvast/main.go +++ b/cmd/quickvast/main.go @@ -1,26 +1,49 @@ package main import ( - "log" "os" + "go.uber.org/zap" "xevion.dev/quickvast/api" "github.com/joho/godotenv" ) func main() { + logger, _ := zap.NewDevelopment() + defer logger.Sync() + + sugar := logger.Sugar() // Load .env file if err := godotenv.Load(); err != nil { - log.Fatal("Error loading .env file") + sugar.Fatal(err) } // Get API key from environment apiKey := os.Getenv("VASTAI_API_KEY") if apiKey == "" { - log.Fatal("VASTAI_API_KEY not found in environment") + sugar.Fatal("VASTAI_API_KEY not found in environment") } // Create client client := api.NewClient(apiKey) + + // Create search + search := api.NewSearch() + // search.CPUCores = api.Ge(8) + + // Perform search + sugar.Infow("Searching", "search", search) + resp, err := client.Search(search) + if err != nil { + sugar.Fatal(err) + } + + // Print offers + sugar.Infof("Offers: %d", len(resp.Offers)) + for _, offer := range resp.Offers { + sugar.Info(offer.String()) + } + + sugar.Info("Done") }