From a0fcf713f06ccd8b239df98590e19fd55a5d1a38 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 01:15:04 +0000 Subject: [PATCH 1/4] Implement Lambda Labs provider methods - Add HTTP client setup with Basic Auth using API key - Implement CreateInstance with SSH key management and instance creation - Implement GetInstance, TerminateInstance, ListInstances, RebootInstance methods - Implement GetInstanceTypes with proper filtering and conversion - Add conversion functions between Lambda Labs API models and v1 interface models - Add proper error handling, status mapping, and HTTP response cleanup - Parse GPU information from Lambda Labs description strings using regex - Convert pricing from cents per hour to currency.Amount objects - Follow existing patterns from dev-plane reference implementation Co-Authored-By: Alec Fong --- internal/lambdalabs/v1/client.go | 14 ++ internal/lambdalabs/v1/instance.go | 219 ++++++++++++++++++++----- internal/lambdalabs/v1/instancetype.go | 145 +++++++++++++--- 3 files changed, 316 insertions(+), 62 deletions(-) diff --git a/internal/lambdalabs/v1/client.go b/internal/lambdalabs/v1/client.go index fa99cad6..b8ed9c06 100644 --- a/internal/lambdalabs/v1/client.go +++ b/internal/lambdalabs/v1/client.go @@ -2,7 +2,9 @@ package v1 import ( "context" + "net/http" + openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs" v1 "github.com/brevdev/compute/pkg/v1" ) @@ -12,15 +14,21 @@ type LambdaLabsClient struct { v1.NotImplCloudClient apiKey string baseURL string + client *openapi.APIClient } var _ v1.CloudClient = &LambdaLabsClient{} // NewLambdaLabsClient creates a new Lambda Labs client func NewLambdaLabsClient(apiKey string) *LambdaLabsClient { + config := openapi.NewConfiguration() + config.HTTPClient = http.DefaultClient + client := openapi.NewAPIClient(config) + return &LambdaLabsClient{ apiKey: apiKey, baseURL: "https://cloud.lambda.ai/api/v1", + client: client, } } @@ -51,3 +59,9 @@ func (c *LambdaLabsClient) GetTenantID() (string, error) { func (c *LambdaLabsClient) GetReferenceID() string { return "lambdalabs-client" } + +func (c *LambdaLabsClient) makeAuthContext(ctx context.Context) context.Context { + return context.WithValue(ctx, openapi.ContextBasicAuth, openapi.BasicAuth{ + UserName: c.apiKey, + }) +} diff --git a/internal/lambdalabs/v1/instance.go b/internal/lambdalabs/v1/instance.go index 64dda9bd..e1de38b3 100644 --- a/internal/lambdalabs/v1/instance.go +++ b/internal/lambdalabs/v1/instance.go @@ -3,74 +3,219 @@ package v1 import ( "context" "fmt" + "strings" "time" + openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs" v1 "github.com/brevdev/compute/pkg/v1" ) // CreateInstance creates a new instance in Lambda Labs // Supported via: POST /api/v1/instance-operations/launch -func (c *LambdaLabsClient) CreateInstance(_ context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { - // TODO: Implement Lambda Labs instance creation - // This would typically involve: - // 1. Validating the instance type and location - // 2. Creating the instance via Lambda Labs API - // 3. Waiting for the instance to be ready - // 4. Returning the instance details +func (c *LambdaLabsClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { + keyPairName := attrs.RefID + if attrs.KeyPairName != nil { + keyPairName = *attrs.KeyPairName + } - return &v1.Instance{ - Name: attrs.Name, - RefID: attrs.RefID, - CreatedAt: time.Now(), - CloudID: v1.CloudProviderInstanceID("lambda-instance-id"), // TODO: Get from API response - Location: attrs.Location, - SubLocation: attrs.SubLocation, - InstanceType: attrs.InstanceType, - ImageID: attrs.ImageID, - DiskSize: attrs.DiskSize, - Status: v1.Status{ - LifecycleStatus: v1.LifecycleStatusRunning, - }, - Tags: attrs.Tags, - }, nil + if attrs.PublicKey != "" { + request := openapi.AddSSHKeyRequest{ + Name: keyPairName, + PublicKey: &attrs.PublicKey, + } + + _, resp, err := c.client.DefaultAPI.AddSSHKey(c.makeAuthContext(ctx)).AddSSHKeyRequest(request).Execute() + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + if err != nil && !strings.Contains(err.Error(), "name must be unique") { + return nil, fmt.Errorf("failed to add SSH key: %w", err) + } + } + + location := attrs.Location + if location == "" { + location = "us-west-1" + } + + quantity := int32(1) + request := openapi.LaunchInstanceRequest{ + RegionName: location, + InstanceTypeName: attrs.InstanceType, + SshKeyNames: []string{keyPairName}, + Quantity: &quantity, + FileSystemNames: []string{}, + } + + if attrs.Name != "" { + request.Name = *openapi.NewNullableString(&attrs.Name) + } + + resp, httpResp, err := c.client.DefaultAPI.LaunchInstance(c.makeAuthContext(ctx)).LaunchInstanceRequest(request).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return nil, fmt.Errorf("failed to launch instance: %w", err) + } + + if len(resp.Data.InstanceIds) != 1 { + return nil, fmt.Errorf("expected 1 instance ID, got %d", len(resp.Data.InstanceIds)) + } + + instanceID := v1.CloudProviderInstanceID(resp.Data.InstanceIds[0]) + return c.GetInstance(ctx, instanceID) } // GetInstance retrieves an instance by ID // Supported via: GET /api/v1/instances/{id} -func (c *LambdaLabsClient) GetInstance(_ context.Context, _ v1.CloudProviderInstanceID) (*v1.Instance, error) { - // TODO: Implement Lambda Labs instance retrieval - return nil, fmt.Errorf("not implemented") +func (c *LambdaLabsClient) GetInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) (*v1.Instance, error) { + resp, httpResp, err := c.client.DefaultAPI.GetInstance(c.makeAuthContext(ctx), string(instanceID)).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return nil, fmt.Errorf("failed to get instance: %w", err) + } + + return convertLambdaLabsInstanceToV1Instance(resp.Data), nil } // TerminateInstance terminates an instance // Supported via: POST /api/v1/instance-operations/terminate -func (c *LambdaLabsClient) TerminateInstance(_ context.Context, _ v1.CloudProviderInstanceID) error { - // TODO: Implement Lambda Labs instance termination - return fmt.Errorf("not implemented") +func (c *LambdaLabsClient) TerminateInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) error { + request := openapi.TerminateInstanceRequest{ + InstanceIds: []string{string(instanceID)}, + } + + _, httpResp, err := c.client.DefaultAPI.TerminateInstance(c.makeAuthContext(ctx)).TerminateInstanceRequest(request).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return fmt.Errorf("failed to terminate instance: %w", err) + } + + return nil } // ListInstances lists all instances // Supported via: GET /api/v1/instances -func (c *LambdaLabsClient) ListInstances(_ context.Context, _ v1.ListInstancesArgs) ([]v1.Instance, error) { - // TODO: Implement Lambda Labs instance listing - return nil, fmt.Errorf("not implemented") +func (c *LambdaLabsClient) ListInstances(ctx context.Context, _ v1.ListInstancesArgs) ([]v1.Instance, error) { + resp, httpResp, err := c.client.DefaultAPI.ListInstances(c.makeAuthContext(ctx)).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return nil, fmt.Errorf("failed to list instances: %w", err) + } + + instances := make([]v1.Instance, 0, len(resp.Data)) + for _, llInstance := range resp.Data { + instance := convertLambdaLabsInstanceToV1Instance(llInstance) + instances = append(instances, *instance) + } + + return instances, nil } // RebootInstance reboots an instance // Supported via: POST /api/v1/instance-operations/restart -func (c *LambdaLabsClient) RebootInstance(_ context.Context, _ v1.CloudProviderInstanceID) error { - // TODO: Implement Lambda Labs instance rebooting - return fmt.Errorf("not implemented") +func (c *LambdaLabsClient) RebootInstance(ctx context.Context, instanceID v1.CloudProviderInstanceID) error { + request := openapi.RestartInstanceRequest{ + InstanceIds: []string{string(instanceID)}, + } + + _, httpResp, err := c.client.DefaultAPI.RestartInstance(c.makeAuthContext(ctx)).RestartInstanceRequest(request).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return fmt.Errorf("failed to reboot instance: %w", err) + } + + return nil } // MergeInstanceForUpdate merges instance data for updates +func convertLambdaLabsInstanceToV1Instance(llInstance openapi.Instance) *v1.Instance { + var publicIP, privateIP, hostname, name string + + if llInstance.Ip.IsSet() { + publicIP = *llInstance.Ip.Get() + } + if llInstance.PrivateIp.IsSet() { + privateIP = *llInstance.PrivateIp.Get() + } + if llInstance.Hostname.IsSet() { + hostname = *llInstance.Hostname.Get() + } + if llInstance.Name.IsSet() { + name = *llInstance.Name.Get() + } + + var cloudCredRefID string + var createdAt time.Time + if name != "" { + parts := strings.Split(name, "--") + if len(parts) > 0 { + cloudCredRefID = parts[0] + } + if len(parts) > 1 { + createdAt, _ = time.Parse("2006-01-02-15-04-05Z07-00", parts[1]) + } + } + + refID := "" + if len(llInstance.SshKeyNames) > 0 { + refID = llInstance.SshKeyNames[0] + } + + return &v1.Instance{ + Name: name, + RefID: refID, + CloudCredRefID: cloudCredRefID, + CreatedAt: createdAt, + CloudID: v1.CloudProviderInstanceID(llInstance.Id), + PublicIP: publicIP, + PrivateIP: privateIP, + PublicDNS: publicIP, + Hostname: hostname, + InstanceType: llInstance.InstanceType.Name, + Status: v1.Status{ + LifecycleStatus: convertLambdaLabsStatusToV1Status(llInstance.Status), + }, + Location: llInstance.Region.Name, + SSHUser: "ubuntu", + SSHPort: 22, + Stoppable: false, + Rebootable: true, + } +} + +func convertLambdaLabsStatusToV1Status(status string) v1.LifecycleStatus { + switch status { + case "booting": + return v1.LifecycleStatusPending + case "active": + return v1.LifecycleStatusRunning + case "terminating": + return v1.LifecycleStatusTerminating + case "terminated": + return v1.LifecycleStatusTerminated + case "error": + return v1.LifecycleStatusFailed + case "unhealthy": + return v1.LifecycleStatusRunning + default: + return v1.LifecycleStatusPending + } +} + func (c *LambdaLabsClient) MergeInstanceForUpdate(_ v1.Instance, newInst v1.Instance) v1.Instance { - // TODO: Implement instance merging logic return newInst } -// MergeInstanceTypeForUpdate merges instance type data for updates func (c *LambdaLabsClient) MergeInstanceTypeForUpdate(_ v1.InstanceType, newIt v1.InstanceType) v1.InstanceType { - // TODO: Implement instance type merging logic return newIt } diff --git a/internal/lambdalabs/v1/instancetype.go b/internal/lambdalabs/v1/instancetype.go index 403e2df4..a4d16f88 100644 --- a/internal/lambdalabs/v1/instancetype.go +++ b/internal/lambdalabs/v1/instancetype.go @@ -2,39 +2,68 @@ package v1 import ( "context" + "fmt" + "regexp" + "strconv" + "strings" "time" "github.com/alecthomas/units" + "github.com/bojanz/currency" + openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs" v1 "github.com/brevdev/compute/pkg/v1" ) // GetInstanceTypes retrieves available instance types from Lambda Labs // Supported via: GET /api/v1/instance-types -func (c *LambdaLabsClient) GetInstanceTypes(_ context.Context, _ v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { - // TODO: Implement Lambda Labs instance type retrieval - // This would typically involve: - // 1. Calling Lambda Labs API to get available instance types - // 2. Filtering based on the provided arguments - // 3. Converting to the standard InstanceType format - - // Example stub implementation - instanceTypes := []v1.InstanceType{ - { - ID: v1.InstanceTypeID("gpu_1x_a10"), - Location: "us-east-1", - AvailableAzs: []string{"us-east-1a", "us-east-1b"}, - SubLocation: "us-east-1a", - Type: "gpu_1x_a10", - SupportedGPUs: []v1.GPU{{Count: 1, Memory: 24 * units.GiB, Manufacturer: "NVIDIA", Name: "A10", Type: "A10"}}, - SupportedStorage: []v1.Storage{{Type: "ssd", Size: 100 * units.GiB}}, - ElasticRootVolume: true, - Memory: 24 * units.GiB, - VCPU: 4, - SupportedArchitectures: []string{"x86_64"}, - IsAvailable: true, - BasePrice: nil, // TODO: Get actual pricing using currency.New - Provider: "lambdalabs", - }, +func (c *LambdaLabsClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { + resp, httpResp, err := c.client.DefaultAPI.InstanceTypes(c.makeAuthContext(ctx)).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return nil, fmt.Errorf("failed to get instance types: %w", err) + } + + var instanceTypes []v1.InstanceType + for _, llInstanceTypeData := range resp.Data { + for _, region := range llInstanceTypeData.RegionsWithCapacityAvailable { + instanceType, err := convertLambdaLabsInstanceTypeToV1InstanceType( + region.Name, + llInstanceTypeData.InstanceType, + true, + ) + if err != nil { + return nil, fmt.Errorf("failed to convert instance type: %w", err) + } + instanceTypes = append(instanceTypes, instanceType) + } + } + + if len(args.Locations) > 0 && !args.Locations.IsAll() { + filtered := make([]v1.InstanceType, 0) + for _, it := range instanceTypes { + for _, loc := range args.Locations { + if it.Location == loc { + filtered = append(filtered, it) + break + } + } + } + instanceTypes = filtered + } + + if len(args.InstanceTypes) > 0 { + filtered := make([]v1.InstanceType, 0) + for _, it := range instanceTypes { + for _, itName := range args.InstanceTypes { + if it.Type == itName { + filtered = append(filtered, it) + break + } + } + } + instanceTypes = filtered } return instanceTypes, nil @@ -48,6 +77,72 @@ func (c *LambdaLabsClient) GetInstanceTypePollTime() time.Duration { // GetLocations retrieves available locations from Lambda Labs // UNSUPPORTED: No location listing endpoints found in Lambda Labs API +func convertLambdaLabsInstanceTypeToV1InstanceType(location string, llInstanceType openapi.InstanceType, isAvailable bool) (v1.InstanceType, error) { + var gpus []v1.GPU + if !strings.Contains(llInstanceType.Description, "CPU") { + gpu := parseGPUFromDescription(llInstanceType.Description) + gpus = append(gpus, gpu) + } + + amount, err := currency.NewAmountFromInt64(int64(llInstanceType.PriceCentsPerHour), "USD") + if err != nil { + return v1.InstanceType{}, fmt.Errorf("failed to create price amount: %w", err) + } + + instanceType := v1.InstanceType{ + Location: location, + Type: llInstanceType.Name, + SupportedGPUs: gpus, + SupportedStorage: []v1.Storage{ + { + Type: "ssd", + Size: units.GiB * units.Base2Bytes(llInstanceType.Specs.StorageGib), + }, + }, + Memory: units.GiB * units.Base2Bytes(llInstanceType.Specs.MemoryGib), + VCPU: llInstanceType.Specs.Vcpus, + SupportedArchitectures: []string{"x86_64"}, + Stoppable: false, + Rebootable: true, + IsAvailable: isAvailable, + BasePrice: &amount, + Provider: "lambdalabs", + } + + instanceType.ID = v1.InstanceTypeID(fmt.Sprintf("lambdalabs-%s-%s", location, llInstanceType.Name)) + + return instanceType, nil +} + +func parseGPUFromDescription(description string) v1.GPU { + countRegex := regexp.MustCompile(`(\d+)x`) + memoryRegex := regexp.MustCompile(`(\d+) GB`) + nameRegex := regexp.MustCompile(`x (.*?) \(`) + + var gpu v1.GPU + + if matches := countRegex.FindStringSubmatch(description); len(matches) > 1 { + if count, err := strconv.ParseInt(matches[1], 10, 32); err == nil { + gpu.Count = int32(count) + } + } + + if matches := memoryRegex.FindStringSubmatch(description); len(matches) > 1 { + if memory, err := strconv.Atoi(matches[1]); err == nil { + gpu.Memory = units.GiB * units.Base2Bytes(memory) + } + } + + if matches := nameRegex.FindStringSubmatch(description); len(matches) > 1 { + gpu.Name = strings.TrimSpace(matches[1]) + gpu.Type = gpu.Name + } + + gpu.Manufacturer = "NVIDIA" + + return gpu +} + func (c *LambdaLabsClient) GetLocations(_ context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { return nil, v1.ErrNotImplemented } From 518ba32778c056cf19650a7788229e6d1974d399 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 05:54:30 +0000 Subject: [PATCH 2/4] Address GitHub comments: implement cloudcred system, update naming schema, and implement GetLocations - Add LambdaLabsCredential struct implementing CloudCredential interface with RefID - Update LambdaLabsClient to use RefID and credential pattern - Update CreateInstance naming schema to match dev-plane pattern with cloudcred RefID - Implement GetLocations using instance types API to extract available regions - Add proper tenant ID generation using API key hash Co-Authored-By: Alec Fong --- internal/lambdalabs/v1/client.go | 58 +++++++++++++++++++++++++- internal/lambdalabs/v1/instance.go | 6 ++- internal/lambdalabs/v1/instancetype.go | 29 ++++++++++++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/internal/lambdalabs/v1/client.go b/internal/lambdalabs/v1/client.go index b8ed9c06..cac8f9c7 100644 --- a/internal/lambdalabs/v1/client.go +++ b/internal/lambdalabs/v1/client.go @@ -2,16 +2,69 @@ package v1 import ( "context" + "crypto/sha256" + "fmt" "net/http" openapi "github.com/brevdev/cloud/internal/lambdalabs/gen/lambdalabs" v1 "github.com/brevdev/compute/pkg/v1" ) +// LambdaLabsCredential implements the CloudCredential interface for Lambda Labs +type LambdaLabsCredential struct { + RefID string + APIKey string +} + +var _ v1.CloudCredential = &LambdaLabsCredential{} + +// NewLambdaLabsCredential creates a new Lambda Labs credential +func NewLambdaLabsCredential(refID, apiKey string) *LambdaLabsCredential { + return &LambdaLabsCredential{ + RefID: refID, + APIKey: apiKey, + } +} + +// GetReferenceID returns the reference ID for this credential +func (c *LambdaLabsCredential) GetReferenceID() string { + return c.RefID +} + +// GetAPIType returns the API type for Lambda Labs +func (c *LambdaLabsCredential) GetAPIType() v1.APIType { + return v1.APITypeGlobal +} + +// GetCloudProviderID returns the cloud provider ID for Lambda Labs +func (c *LambdaLabsCredential) GetCloudProviderID() v1.CloudProviderID { + return "lambdalabs" +} + +// GetTenantID returns the tenant ID for Lambda Labs +func (c *LambdaLabsCredential) GetTenantID() (string, error) { + return fmt.Sprintf("lambdalabs-%x", sha256.Sum256([]byte(c.APIKey))), nil +} + +// GetCapabilities returns the capabilities for Lambda Labs +func (c *LambdaLabsCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) { + return []v1.Capability{ + v1.CapabilityCreateInstance, + v1.CapabilityTerminateInstance, + v1.CapabilityRebootInstance, + }, nil +} + +// MakeClient creates a new Lambda Labs client from this credential +func (c *LambdaLabsCredential) MakeClient(_ context.Context, _ string) (v1.CloudClient, error) { + return NewLambdaLabsClient(c.RefID, c.APIKey), nil +} + // LambdaLabsClient implements the CloudClient interface for Lambda Labs // It embeds NotImplCloudClient to handle unsupported features type LambdaLabsClient struct { v1.NotImplCloudClient + refID string apiKey string baseURL string client *openapi.APIClient @@ -20,12 +73,13 @@ type LambdaLabsClient struct { var _ v1.CloudClient = &LambdaLabsClient{} // NewLambdaLabsClient creates a new Lambda Labs client -func NewLambdaLabsClient(apiKey string) *LambdaLabsClient { +func NewLambdaLabsClient(refID, apiKey string) *LambdaLabsClient { config := openapi.NewConfiguration() config.HTTPClient = http.DefaultClient client := openapi.NewAPIClient(config) return &LambdaLabsClient{ + refID: refID, apiKey: apiKey, baseURL: "https://cloud.lambda.ai/api/v1", client: client, @@ -57,7 +111,7 @@ func (c *LambdaLabsClient) GetTenantID() (string, error) { // GetReferenceID returns the reference ID for this client func (c *LambdaLabsClient) GetReferenceID() string { - return "lambdalabs-client" + return c.refID } func (c *LambdaLabsClient) makeAuthContext(ctx context.Context) context.Context { diff --git a/internal/lambdalabs/v1/instance.go b/internal/lambdalabs/v1/instance.go index e1de38b3..9ecc05f7 100644 --- a/internal/lambdalabs/v1/instance.go +++ b/internal/lambdalabs/v1/instance.go @@ -10,6 +10,8 @@ import ( v1 "github.com/brevdev/compute/pkg/v1" ) +const lambdaLabsTimeNameFormat = "2006-01-02-15-04-05Z07-00" + // CreateInstance creates a new instance in Lambda Labs // Supported via: POST /api/v1/instance-operations/launch func (c *LambdaLabsClient) CreateInstance(ctx context.Context, attrs v1.CreateInstanceAttrs) (*v1.Instance, error) { @@ -47,9 +49,11 @@ func (c *LambdaLabsClient) CreateInstance(ctx context.Context, attrs v1.CreateIn FileSystemNames: []string{}, } + name := fmt.Sprintf("%s--%s", c.GetReferenceID(), time.Now().UTC().Format(lambdaLabsTimeNameFormat)) if attrs.Name != "" { - request.Name = *openapi.NewNullableString(&attrs.Name) + name = fmt.Sprintf("%s--%s--%s", c.GetReferenceID(), attrs.Name, time.Now().UTC().Format(lambdaLabsTimeNameFormat)) } + request.Name = *openapi.NewNullableString(&name) resp, httpResp, err := c.client.DefaultAPI.LaunchInstance(c.makeAuthContext(ctx)).LaunchInstanceRequest(request).Execute() if httpResp != nil { diff --git a/internal/lambdalabs/v1/instancetype.go b/internal/lambdalabs/v1/instancetype.go index a4d16f88..978bc384 100644 --- a/internal/lambdalabs/v1/instancetype.go +++ b/internal/lambdalabs/v1/instancetype.go @@ -143,6 +143,31 @@ func parseGPUFromDescription(description string) v1.GPU { return gpu } -func (c *LambdaLabsClient) GetLocations(_ context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { - return nil, v1.ErrNotImplemented +func (c *LambdaLabsClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { + resp, httpResp, err := c.client.DefaultAPI.InstanceTypes(c.makeAuthContext(ctx)).Execute() + if httpResp != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return nil, fmt.Errorf("failed to get instance types: %w", err) + } + + locationMap := make(map[string]bool) + for _, llInstanceTypeData := range resp.Data { + for _, region := range llInstanceTypeData.RegionsWithCapacityAvailable { + locationMap[region.Name] = true + } + } + + var locations []v1.Location + for locationName := range locationMap { + locations = append(locations, v1.Location{ + Name: locationName, + Description: fmt.Sprintf("Lambda Labs region: %s", locationName), + Available: true, + Country: "USA", + }) + } + + return locations, nil } From 87d0e7346d16c651b7d18574e6f1ca570e41c551 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 06:05:37 +0000 Subject: [PATCH 3/4] Address GitHub comments: move GetCapabilities to capabilities.go and use hardcoded locations - Move LambdaLabsCredential.GetCapabilities implementation from client.go to capabilities.go - Replace dynamic GetLocations implementation with hardcoded location data as requested - Use JSON unmarshaling approach matching dev-plane pattern - Fix gofumpt formatting issues Co-Authored-By: Alec Fong --- internal/lambdalabs/v1/capabilities.go | 9 +++++ internal/lambdalabs/v1/client.go | 9 ----- internal/lambdalabs/v1/instancetype.go | 54 +++++++++++++++++--------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/internal/lambdalabs/v1/capabilities.go b/internal/lambdalabs/v1/capabilities.go index 7025cd81..d38941f2 100644 --- a/internal/lambdalabs/v1/capabilities.go +++ b/internal/lambdalabs/v1/capabilities.go @@ -30,3 +30,12 @@ func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, return capabilities, nil } + +// GetCapabilities returns the capabilities for Lambda Labs credential +func (c *LambdaLabsCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) { + return []v1.Capability{ + v1.CapabilityCreateInstance, + v1.CapabilityTerminateInstance, + v1.CapabilityRebootInstance, + }, nil +} diff --git a/internal/lambdalabs/v1/client.go b/internal/lambdalabs/v1/client.go index cac8f9c7..7f10897d 100644 --- a/internal/lambdalabs/v1/client.go +++ b/internal/lambdalabs/v1/client.go @@ -46,15 +46,6 @@ func (c *LambdaLabsCredential) GetTenantID() (string, error) { return fmt.Sprintf("lambdalabs-%x", sha256.Sum256([]byte(c.APIKey))), nil } -// GetCapabilities returns the capabilities for Lambda Labs -func (c *LambdaLabsCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) { - return []v1.Capability{ - v1.CapabilityCreateInstance, - v1.CapabilityTerminateInstance, - v1.CapabilityRebootInstance, - }, nil -} - // MakeClient creates a new Lambda Labs client from this credential func (c *LambdaLabsCredential) MakeClient(_ context.Context, _ string) (v1.CloudClient, error) { return NewLambdaLabsClient(c.RefID, c.APIKey), nil diff --git a/internal/lambdalabs/v1/instancetype.go b/internal/lambdalabs/v1/instancetype.go index 978bc384..09f788d1 100644 --- a/internal/lambdalabs/v1/instancetype.go +++ b/internal/lambdalabs/v1/instancetype.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "encoding/json" "fmt" "regexp" "strconv" @@ -143,29 +144,46 @@ func parseGPUFromDescription(description string) v1.GPU { return gpu } -func (c *LambdaLabsClient) GetLocations(ctx context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { - resp, httpResp, err := c.client.DefaultAPI.InstanceTypes(c.makeAuthContext(ctx)).Execute() - if httpResp != nil { - defer func() { _ = httpResp.Body.Close() }() - } - if err != nil { - return nil, fmt.Errorf("failed to get instance types: %w", err) - } +const lambdaLocationsData = `[ + {"location_name": "us-west-1", "description": "California, USA", "country": "USA"}, + {"location_name": "us-west-2", "description": "Arizona, USA", "country": "USA"}, + {"location_name": "us-west-3", "description": "Utah, USA", "country": "USA"}, + {"location_name": "us-south-1", "description": "Texas, USA", "country": "USA"}, + {"location_name": "us-east-1", "description": "Virginia, USA", "country": "USA"}, + {"location_name": "us-midwest-1", "description": "Illinois, USA", "country": "USA"}, + {"location_name": "australia-southeast-1", "description": "Australia", "country": "AUS"}, + {"location_name": "europe-central-1", "description": "Germany", "country": "DEU"}, + {"location_name": "asia-south-1", "description": "India", "country": "IND"}, + {"location_name": "me-west-1", "description": "Israel", "country": "ISR"}, + {"location_name": "europe-south-1", "description": "Italy", "country": "ITA"}, + {"location_name": "asia-northeast-1", "description": "Osaka, Japan", "country": "JPN"}, + {"location_name": "asia-northeast-2", "description": "Tokyo, Japan", "country": "JPN"}, + {"location_name": "us-east-3", "description": "Washington D.C, USA", "country": "USA"}, + {"location_name": "us-east-2", "description": "Washington D.C, USA", "country": "USA"}, + {"location_name": "australia-east-1", "description": "Sydney, Australia", "country": "AUS"}, + {"location_name": "us-south-3", "description": "Central Texas, USA", "country": "USA"}, + {"location_name": "us-south-2", "description": "North Texas, USA", "country": "USA"} +]` + +type LambdaLocation struct { + LocationName string `json:"location_name"` + Description string `json:"description"` + Country string `json:"country"` +} - locationMap := make(map[string]bool) - for _, llInstanceTypeData := range resp.Data { - for _, region := range llInstanceTypeData.RegionsWithCapacityAvailable { - locationMap[region.Name] = true - } +func (c *LambdaLabsClient) GetLocations(_ context.Context, _ v1.GetLocationsArgs) ([]v1.Location, error) { + var regionData []LambdaLocation + if err := json.Unmarshal([]byte(lambdaLocationsData), ®ionData); err != nil { + return nil, fmt.Errorf("failed to parse location data: %w", err) } - var locations []v1.Location - for locationName := range locationMap { + locations := make([]v1.Location, 0, len(regionData)) + for _, region := range regionData { locations = append(locations, v1.Location{ - Name: locationName, - Description: fmt.Sprintf("Lambda Labs region: %s", locationName), + Name: region.LocationName, + Description: region.Description, Available: true, - Country: "USA", + Country: region.Country, }) } From d8c2d9d0c817eb51e5976cb52a24201873352907 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 06:11:03 +0000 Subject: [PATCH 4/4] Combine client and credential capabilities, remove firewall support - Create unified getLambdaLabsCapabilities() function to eliminate code duplication - Both LambdaLabsClient and LambdaLabsCredential now use the same capability set - Remove v1.CapabilityModifyFirewall since Lambda Labs doesn't support it at project level - Addresses GitHub comment from theFong about combining capabilities Co-Authored-By: Alec Fong --- internal/lambdalabs/v1/capabilities.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/internal/lambdalabs/v1/capabilities.go b/internal/lambdalabs/v1/capabilities.go index d38941f2..ba348ab5 100644 --- a/internal/lambdalabs/v1/capabilities.go +++ b/internal/lambdalabs/v1/capabilities.go @@ -6,10 +6,10 @@ import ( v1 "github.com/brevdev/compute/pkg/v1" ) -// GetCapabilities returns the capabilities of Lambda Labs +// getLambdaLabsCapabilities returns the unified capabilities for Lambda Labs // Based on API documentation at https://cloud.lambda.ai/api/v1/openapi.json -func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) { - capabilities := v1.Capabilities{ +func getLambdaLabsCapabilities() v1.Capabilities { + return v1.Capabilities{ // SUPPORTED FEATURES (with API evidence): // Instance Management @@ -18,24 +18,21 @@ func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, v1.CapabilityCreateTerminateInstance, // Combined create/terminate capability v1.CapabilityRebootInstance, // POST /api/v1/instance-operations/restart - // Firewall Management - v1.CapabilityModifyFirewall, // Firewall rulesets API available - // UNSUPPORTED FEATURES (no API evidence found): + // - v1.CapabilityModifyFirewall // Firewall management is project-level, not instance-level // - v1.CapabilityStopStartInstance // No stop/start endpoints // - v1.CapabilityResizeInstanceVolume // No volume resizing endpoints // - v1.CapabilityMachineImage // No image endpoints // - v1.CapabilityTags // No tagging endpoints } +} - return capabilities, nil +// GetCapabilities returns the capabilities of Lambda Labs client +func (c *LambdaLabsClient) GetCapabilities(_ context.Context) (v1.Capabilities, error) { + return getLambdaLabsCapabilities(), nil } // GetCapabilities returns the capabilities for Lambda Labs credential func (c *LambdaLabsCredential) GetCapabilities(_ context.Context) (v1.Capabilities, error) { - return []v1.Capability{ - v1.CapabilityCreateInstance, - v1.CapabilityTerminateInstance, - v1.CapabilityRebootInstance, - }, nil + return getLambdaLabsCapabilities(), nil }