From 4f6d90a6fd7c2f762529b266b906942c70d3ddbd Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 12:27:50 -0700 Subject: [PATCH 01/17] docs: design ARM-aware cloud catalogs --- ...07-27-cloud-arm-instance-catalog-design.md | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md diff --git a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md new file mode 100644 index 0000000..a2a5185 --- /dev/null +++ b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md @@ -0,0 +1,208 @@ +# Cloud ARM Instance Catalog Design + +## Goal + +Make the Cloud SDK's instance and image catalogs architecture-complete without +changing dev-plane's current x86-only policy. + +Cloud providers must: + +- return every known instance type when no architecture filter is supplied; +- populate `SupportedArchitectures` with truthful canonical values; +- honor a caller-provided architecture filter; +- report an unknown architecture as `unknown` instead of silently treating it + as `x86_64`. + +This preserves the intended boundary: + +```text +Cloud reports provider capabilities. +Callers decide which capabilities their product currently supports. +``` + +Dev-plane will remain unchanged. Its current request excludes ARM, so ARM +instance types added by this change will not pass into its synchronized catalog. + +## Scope + +### Shared architecture classification + +Add a small, case-insensitive helper that recognizes NVIDIA Grace products from +their GPU model: + +- `GH*` is ARM64. +- `GB*` is ARM64. +- A plain `B*` model does not imply ARM64. + +The helper should answer only whether a GPU model identifies a Grace system. +Each provider remains responsible for choosing the fallback architecture based +on its own catalog contract. + +Use canonical Cloud values: + +- `x86_64` +- `arm64` +- `unknown` + +### Launchpad + +Launchpad already supplies an authoritative `SystemArch` field and maps it to +the Cloud architecture values. No production mapping change is needed. + +Add unit coverage proving: + +- `SystemArchAMD64` becomes `x86_64`; +- `SystemArchARM64` becomes `arm64`; +- caller-provided ARM exclusions remove ARM catalog entries. + +### Shadeform + +Retain the existing Grace inference while moving it to the shared helper. +Normalize model casing before classification. + +Add conversion coverage for: + +- GH200 as ARM64; +- GB200 as ARM64; +- lowercase Grace model names as ARM64; +- H100 and B200 as x86_64. + +### LambdaLabs + +Replace the unconditional x86 assignment with architecture derived from the +parsed GPU model: + +- recognized Grace products become ARM64; +- other currently supported Lambda instance types remain x86_64. + +The existing architecture filter remains authoritative and must exclude a +GH200 type when ARM is excluded. + +### FluidStack + +Populate `SupportedArchitectures` from `GpuModel` using the same Grace rule. +Known non-Grace and CPU offerings remain x86_64. + +FluidStack currently ignores `GetInstanceTypeArgs`. Add architecture-filter +handling without broadening this change into location or instance-name +filtering. This is required so dev-plane's existing ARM exclusion remains +effective. + +### Nebius instance types + +Nebius does not currently expose an architecture field in its Platform API. +Use a provider-local mapping for the platform IDs that Cloud supports. + +The currently documented Nebius platforms use Intel or AMD host CPUs, including +the B200 and B300 platforms, and must therefore report `x86_64`. An unrecognized +platform must report `unknown`. + +Populate `SupportedArchitectures` during instance-type construction. Replace +the current type-name inference and include-only filtering with filtering over +the populated architecture metadata using `ArchitectureFilter.IsAllowed`, so +both include and exclude filters work. + +Do not invent a synthetic Nebius ARM platform. Add a real ARM mapping only when +Nebius publishes an ARM platform identifier or exposes an authoritative +architecture field. + +### Nebius images + +Remove the catalog-level x86 restriction: + +- an unfiltered image request returns all architectures; +- use the Nebius `ImageSpec.CpuArchitecture` enum as the authoritative source; +- retain labels and image-name parsing only as compatibility fallbacks; +- use `unknown` when no source identifies the architecture. + +This changes image discovery only. Architecture-aware boot-image selection is +part of the later provisioning work. + +### SFCompute and SFComputeV2 + +Make no production classification change. The current providers expose H100 +and H200 offerings backed by x86-only image requirements, and their metadata +already reports `x86_64`. + +Add focused regression coverage where practical so this is recorded as a +provider capability rather than an accidental ARM restriction. + +Future SFCompute ARM support requires an authoritative Grace SKU plus +architecture-compatible image and create-path support; it is not part of this +catalog change. + +### TestKube + +Defer ARM advertising. + +Safe TestKube ARM support requires: + +- a separately named ARM instance type; +- `SupportedArchitectures: [arm64]`; +- a `kubernetes.io/arch: arm64` pod node selector; +- a published and verified `linux/arm64` image manifest. + +The existing `test.ok.cpu` type remains x86-only. A single type advertising +both architectures would be unsafe because it could pass an x86 filter and +then schedule onto an ARM node. + +## Data Flow + +For Cloud callers: + +```text +provider API response + -> provider-specific architecture extraction + -> canonical Cloud architecture + -> optional ArchitectureFilter + -> returned InstanceType or Image +``` + +An unfiltered call returns x86, ARM, and unknown entries. A filtered call +returns only entries whose architecture is allowed. dev-plane will continue to +send its existing x86-only filter and therefore will not receive ARM entries. + +## Error and Unknown-Data Handling + +- Do not fail an entire provider catalog because one item has an unknown + architecture. +- Preserve that item as `unknown` when the provider otherwise supports it. +- Never default an unrecognized provider platform or image to x86. +- Continue returning provider/API errors using the existing error-wrapping + conventions. + +## Testing + +Follow test-first development for every behavior change. + +Focused tests will cover: + +- shared Grace GPU classification, including casing and B200 non-ARM behavior; +- Launchpad authoritative AMD64 and ARM64 conversion; +- Shadeform GH/GB conversion; +- LambdaLabs GH200 conversion and architecture filtering; +- FluidStack conversion and architecture filtering; +- Nebius known x86 and unknown platform mappings; +- Nebius include and exclude filter behavior; +- Nebius authoritative AMD64/ARM64 image metadata and unfiltered image listing; +- existing SFCompute x86 metadata where a focused unit seam exists. + +Run package-level tests while iterating, then: + +```bash +go test ./v1/... +go test ./... +``` + +Run `gofmt` on every touched Go file and run the repository's configured +linting if practical. + +## Non-Goals + +- No dev-plane changes. +- No removal of the public `ArchitectureFilter` contract. +- No enabling ARM instance creation in dev-plane. +- No architecture-aware provider boot-image selection beyond image catalog + metadata. +- No TestKube image publishing or ARM scheduling change. +- No Verb-mode changes. From a89fbe9a564e8ff90223b0c75bd16c180de57534 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 12:49:38 -0700 Subject: [PATCH 02/17] docs: plan Cloud ARM catalog implementation --- .../2026-07-27-cloud-arm-instance-catalog.md | 1267 +++++++++++++++++ 1 file changed, 1267 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md diff --git a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md new file mode 100644 index 0000000..38777ec --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md @@ -0,0 +1,1267 @@ +# Cloud ARM Instance Catalog Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make Cloud return ARM-capable instance and image catalog entries with truthful canonical architecture metadata while preserving dev-plane's existing ARM exclusion. + +**Architecture:** Keep architecture discovery inside each provider adapter, normalize shared NVIDIA Grace model detection in `v1`, and apply the existing caller-owned architecture filter after metadata is populated. Providers with authoritative architecture fields retain those fields as the source of truth; inferred or mapped providers return `unknown` when the architecture cannot be established. + +**Tech Stack:** Go 1.25, generated provider SDK clients, `testify`, package-level Go tests, `gofmt`, and `golangci-lint`. + +## Global Constraints + +- The approved design is + [`docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md`](../specs/2026-07-27-cloud-arm-instance-catalog-design.md). +- Do not modify dev-plane. Its existing `ArchitectureFilter` continues to exclude + ARM from the synchronized catalog. +- Do not change or remove the public `ArchitectureFilter` contract. +- Do not enable ARM provisioning, image selection, TestKube ARM scheduling, or + Verb mode. +- An unfiltered Cloud call returns all known entries. A filtered call honors the + caller's filter. +- Emit only canonical values: `x86_64`, `arm64`, or `unknown`. +- Do not classify B200 or B300 as ARM merely because they are Blackwell GPUs. +- Nebius's current documented platform IDs are mapped explicitly. The current + platform table documents Intel or AMD host CPUs for all of them: + . +- Follow red-green-refactor for every behavior change. Characterization tests + for already-correct providers are expected to pass immediately. +- Run `gofmt` on every touched Go file. Do not edit generated provider clients. + +## File and Interface Map + +| Area | Existing contract | Planned touchpoint | +| --- | --- | --- | +| Shared types | `v1.Architecture`, `ArchitectureFilter.IsAllowed` | Add shared Grace model classifier | +| Launchpad | Authoritative `SystemArch` | Add mapping and filter characterization tests | +| Shadeform | GPU-name inference | Use shared classifier | +| LambdaLabs | Parsed GPU description, currently hardcoded x86 | Classify parsed Grace GPU models | +| FluidStack | `GpuModel`, currently no architecture metadata/filter | Populate metadata and honor only architecture filtering | +| Nebius instances | Platform IDs, name-based fallback to x86 | Explicit platform map, metadata-driven include/exclude filtering | +| Nebius images | `ImageSpec.CpuArchitecture`, currently ignored | Prefer enum, remove unfiltered x86 default | +| SFCompute v1/v2 | Static H100/H200 x86 metadata | Add regression coverage only | + +--- + +### Task 1: Add the shared NVIDIA Grace classifier + +**Files:** + +- Modify: `v1/instancetype.go` +- Modify: `v1/instancetype_test.go` + +- [ ] **Step 1: Write the failing classifier test** + +Append this table test to `v1/instancetype_test.go`: + +```go +func TestIsNVIDIAGraceGPU(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + gpuModel string + want bool + }{ + {name: "GH200", gpuModel: "GH200", want: true}, + {name: "GB200", gpuModel: "GB200", want: true}, + {name: "lowercase GH200", gpuModel: "gh200", want: true}, + {name: "trimmed GB200", gpuModel: " gb200 ", want: true}, + {name: "vendor-prefixed GH200", gpuModel: "NVIDIA GH200", want: true}, + {name: "H100", gpuModel: "H100", want: false}, + {name: "B200", gpuModel: "B200", want: false}, + {name: "empty", gpuModel: "", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := IsNVIDIAGraceGPU(tt.gpuModel); got != tt.want { + t.Fatalf("IsNVIDIAGraceGPU(%q) = %v, want %v", tt.gpuModel, got, tt.want) + } + }) + } +} +``` + +- [ ] **Step 2: Run the focused test and confirm the red state** + +Run: + +```bash +go test ./v1 -run '^TestIsNVIDIAGraceGPU$' -count=1 +``` + +Expected: compilation fails because `IsNVIDIAGraceGPU` is undefined. + +- [ ] **Step 3: Implement the smallest shared classifier** + +Add this function beside the existing architecture normalization helpers in +`v1/instancetype.go`: + +```go +func IsNVIDIAGraceGPU(gpuModel string) bool { + normalizedModel := strings.ToUpper(strings.TrimSpace(gpuModel)) + normalizedModel = strings.TrimPrefix(normalizedModel, "NVIDIA ") + return strings.HasPrefix(normalizedModel, "GH") || + strings.HasPrefix(normalizedModel, "GB") +} +``` + +The function deliberately answers only whether the model is a Grace product. +It does not choose a fallback architecture. + +- [ ] **Step 4: Format and prove the green state** + +Run: + +```bash +gofmt -w v1/instancetype.go v1/instancetype_test.go +go test ./v1 -run '^(TestGetArchitectureAliases|TestIsNVIDIAGraceGPU)$' -count=1 +``` + +Expected: both tests pass. + +- [ ] **Step 5: Commit the shared behavior** + +```bash +git add v1/instancetype.go v1/instancetype_test.go +git commit -m "feat: classify NVIDIA Grace GPU models" +``` + +--- + +### Task 2: Lock Launchpad's authoritative architecture behavior + +**Files:** + +- Modify: `v1/providers/launchpad/instancetype_test.go` +- Verify only: `v1/providers/launchpad/instancetype.go` + +- [ ] **Step 1: Add mapping and caller-filter characterization tests** + +Add the generated Launchpad package import: + +```go +openapi "github.com/brevdev/cloud/v1/providers/launchpad/gen/launchpad" +``` + +Then append: + +```go +func TestLaunchpadArchitectureToArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + raw openapi.SystemArchEnum + want v1.Architecture + }{ + {name: "AMD64", raw: openapi.SystemArchAMD64, want: v1.ArchitectureX86_64}, + {name: "ARM64", raw: openapi.SystemArchARM64, want: v1.ArchitectureARM64}, + {name: "unknown", raw: openapi.SystemArchEnum("riscv64"), want: v1.ArchitectureUnknown}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, launchpadArchitectureToArchitecture(tt.raw)) + }) + } +} + +func TestLaunchpadArchitectureFilterExcludesARM(t *testing.T) { + t.Parallel() + + instanceType := v1.InstanceType{ + SupportedArchitectures: []v1.Architecture{ + launchpadArchitectureToArchitecture(openapi.SystemArchARM64), + }, + } + args := v1.GetInstanceTypeArgs{ + ArchitectureFilter: &v1.ArchitectureFilter{ + ExcludeArchitectures: []v1.Architecture{v1.ArchitectureARM64}, + }, + } + + require.False(t, v1.IsSelectedByArgs(instanceType, args)) +} +``` + +- [ ] **Step 2: Run the characterization tests** + +Run: + +```bash +go test ./v1/providers/launchpad -run '^TestLaunchpadArchitecture' -count=1 +``` + +Expected: tests pass without production changes because Launchpad already uses +its authoritative `SystemArch` value and the common filter. + +- [ ] **Step 3: Commit the contract tests** + +```bash +git add v1/providers/launchpad/instancetype_test.go +git commit -m "test: lock Launchpad architecture mapping" +``` + +--- + +### Task 3: Move Shadeform Grace inference to the shared classifier + +**Files:** + +- Modify: `v1/providers/shadeform/instancetype.go` +- Modify: `v1/providers/shadeform/instancetype_test.go` + +- [ ] **Step 1: Write the failing case-normalization test** + +Append: + +```go +func TestShadeformArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + gpuModel string + want v1.Architecture + }{ + {gpuModel: "GH200", want: v1.ArchitectureARM64}, + {gpuModel: "GB200", want: v1.ArchitectureARM64}, + {gpuModel: "gh200", want: v1.ArchitectureARM64}, + {gpuModel: "gb200", want: v1.ArchitectureARM64}, + {gpuModel: "H100", want: v1.ArchitectureX86_64}, + {gpuModel: "B200", want: v1.ArchitectureX86_64}, + } + + for _, tt := range tests { + t.Run(tt.gpuModel, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, shadeformArchitecture(tt.gpuModel)) + }) + } +} +``` + +- [ ] **Step 2: Run the focused test and confirm the red state** + +Run: + +```bash +go test ./v1/providers/shadeform -run '^TestShadeformArchitecture$' -count=1 +``` + +Expected: lowercase GH200 and GB200 cases fail because the current check is +case-sensitive. + +- [ ] **Step 3: Delegate classification to the shared helper** + +Replace `shadeformArchitecture` with: + +```go +func shadeformArchitecture(gpuName string) v1.Architecture { + if v1.IsNVIDIAGraceGPU(gpuName) { + return v1.ArchitectureARM64 + } + return v1.ArchitectureX86_64 +} +``` + +Keep the provider-owned x86 fallback; only recognition is shared. + +- [ ] **Step 4: Format and run the package tests** + +Run: + +```bash +gofmt -w v1/providers/shadeform/instancetype.go v1/providers/shadeform/instancetype_test.go +go test ./v1/providers/shadeform -count=1 +``` + +Expected: all Shadeform tests pass. + +- [ ] **Step 5: Commit the provider change** + +```bash +git add v1/providers/shadeform/instancetype.go v1/providers/shadeform/instancetype_test.go +git commit -m "feat: normalize Shadeform Grace architectures" +``` + +--- + +### Task 4: Classify LambdaLabs Grace offerings and preserve filtering + +**Files:** + +- Modify: `v1/providers/lambdalabs/instancetype.go` +- Modify: `v1/providers/lambdalabs/instancetype_test.go` + +- [ ] **Step 1: Write failing conversion and end-to-end filter tests** + +Append these tests: + +```go +func TestConvertLambdaLabsInstanceTypeArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + description string + want v1.Architecture + }{ + {name: "GH200", description: "1x GH200 (96 GB)", want: v1.ArchitectureARM64}, + {name: "NVIDIA GH200", description: "1x NVIDIA GH200 (96 GB)", want: v1.ArchitectureARM64}, + {name: "H100", description: "1x H100 (80 GB SXM5)", want: v1.ArchitectureX86_64}, + {name: "B200", description: "1x B200 (192 GB SXM6)", want: v1.ArchitectureX86_64}, + {name: "CPU", description: "4x CPU cores", want: v1.ArchitectureX86_64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + instanceType := createMockLambdaLabsInstanceType( + "test-"+tt.name, + tt.description, + tt.name, + 100, + ) + + got, err := convertLambdaLabsInstanceTypeToV1InstanceType( + "us-west-1", + instanceType, + true, + ) + require.NoError(t, err) + require.Equal(t, []v1.Architecture{tt.want}, got.SupportedArchitectures) + }) + } +} + +func TestLambdaLabsClient_GetInstanceTypes_ExcludeARM(t *testing.T) { + client, cleanup := setupMockClient() + defer cleanup() + + mockResponse := createMockInstanceTypeResponse() + mockResponse.Data["gpu_1x_gh200"] = openapi.InstanceTypes200ResponseDataValue{ + InstanceType: createMockLambdaLabsInstanceType( + "gpu_1x_gh200", + "1x GH200 (96 GB)", + "GH200", + 229, + ), + RegionsWithCapacityAvailable: []openapi.Region{ + createMockRegion("us-west-1", "US West 1"), + }, + } + httpmock.RegisterResponder( + "GET", + "https://cloud.lambda.ai/api/v1/instance-types", + httpmock.NewJsonResponderOrPanic(200, mockResponse), + ) + + instanceTypes, err := client.GetInstanceTypes( + context.Background(), + v1.GetInstanceTypeArgs{ + ArchitectureFilter: &v1.ArchitectureFilter{ + ExcludeArchitectures: []v1.Architecture{v1.ArchitectureARM64}, + }, + }, + ) + require.NoError(t, err) + require.Nil(t, findInstanceTypeByName(instanceTypes, "gpu_1x_gh200")) + require.NotNil(t, findInstanceTypeByName(instanceTypes, "gpu_1x_a10")) +} +``` + +- [ ] **Step 2: Run the focused tests and confirm the red state** + +Run: + +```bash +go test ./v1/providers/lambdalabs -run 'Architecture|ExcludeARM' -count=1 +``` + +Expected: GH200 is reported as x86 and survives the ARM exclusion. + +- [ ] **Step 3: Derive architecture from the parsed GPU model** + +After parsing `gpus` and before constructing the returned instance type, add: + +```go +architecture := v1.ArchitectureX86_64 +if len(gpus) > 0 && v1.IsNVIDIAGraceGPU(gpus[0].Name) { + architecture = v1.ArchitectureARM64 +} +``` + +Then replace the hardcoded field with: + +```go +SupportedArchitectures: []v1.Architecture{architecture}, +``` + +CPU-only and non-Grace offerings deliberately retain the provider-owned x86 +fallback. + +- [ ] **Step 4: Format and run the complete LambdaLabs package** + +Run: + +```bash +gofmt -w v1/providers/lambdalabs/instancetype.go v1/providers/lambdalabs/instancetype_test.go +go test ./v1/providers/lambdalabs -count=1 +``` + +Expected: all LambdaLabs tests pass. + +- [ ] **Step 5: Commit the provider change** + +```bash +git add v1/providers/lambdalabs/instancetype.go v1/providers/lambdalabs/instancetype_test.go +git commit -m "feat: report LambdaLabs Grace architectures" +``` + +--- + +### Task 5: Populate and filter FluidStack architecture metadata + +**Files:** + +- Modify: `v1/providers/fluidstack/instancetype.go` +- Create: `v1/providers/fluidstack/instancetype_test.go` + +- [ ] **Step 1: Write failing converter and filter tests** + +Create `v1/providers/fluidstack/instancetype_test.go`: + +```go +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" + openapi "github.com/brevdev/cloud/v1/providers/fluidstack/gen/fluidstack" +) + +func TestConvertFluidStackInstanceTypeArchitecture(t *testing.T) { + t.Parallel() + + gh200 := "gh200" + b200 := "B200" + tests := []struct { + name string + gpuModel *string + want cloudv1.Architecture + }{ + {name: "GH200", gpuModel: &gh200, want: cloudv1.ArchitectureARM64}, + {name: "B200", gpuModel: &b200, want: cloudv1.ArchitectureX86_64}, + {name: "CPU", gpuModel: nil, want: cloudv1.ArchitectureX86_64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gpuCount := int32(1) + if tt.gpuModel == nil { + gpuCount = 0 + } + got := convertFluidStackInstanceTypeToV1InstanceType( + "", + openapi.InstanceType{ + Name: "test-" + tt.name, + Cpu: 64, + Memory: "432GB", + GpuModel: tt.gpuModel, + GpuCount: &gpuCount, + }, + true, + ) + require.Equal(t, []cloudv1.Architecture{tt.want}, got.SupportedArchitectures) + }) + } +} + +func TestFilterFluidStackInstanceTypesByArchitecture(t *testing.T) { + t.Parallel() + + x86Type := cloudv1.InstanceType{ + Type: "x86", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + } + armType := cloudv1.InstanceType{ + Type: "arm", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + } + instanceTypes := []cloudv1.InstanceType{x86Type, armType} + + require.Equal( + t, + instanceTypes, + filterInstanceTypesByArchitecture(instanceTypes, nil), + ) + require.Equal( + t, + []cloudv1.InstanceType{x86Type}, + filterInstanceTypesByArchitecture( + instanceTypes, + &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + }, + ), + ) +} +``` + +- [ ] **Step 2: Run the focused tests and confirm the red state** + +Run: + +```bash +go test ./v1/providers/fluidstack -run 'Architecture' -count=1 +``` + +Expected: the filter helper is undefined and the converter emits no +`SupportedArchitectures`. + +- [ ] **Step 3: Populate architecture metadata during conversion** + +Before the returned `v1.InstanceType` literal, add: + +```go +architecture := v1.ArchitectureX86_64 +if fsInstanceType.GpuModel != nil && + v1.IsNVIDIAGraceGPU(*fsInstanceType.GpuModel) { + architecture = v1.ArchitectureARM64 +} +``` + +Add this field to the returned literal: + +```go +SupportedArchitectures: []v1.Architecture{architecture}, +``` + +- [ ] **Step 4: Add architecture-only filtering** + +Change the `GetInstanceTypes` signature to retain `args`: + +```go +func (c *FluidStackClient) GetInstanceTypes( + ctx context.Context, + args v1.GetInstanceTypeArgs, +) ([]v1.InstanceType, error) { +``` + +Return the architecture-filtered values: + +```go +return filterInstanceTypesByArchitecture( + instanceTypes, + args.ArchitectureFilter, +), nil +``` + +Add this provider-local helper: + +```go +func filterInstanceTypesByArchitecture( + instanceTypes []v1.InstanceType, + filter *v1.ArchitectureFilter, +) []v1.InstanceType { + if filter == nil { + return instanceTypes + } + + filtered := make([]v1.InstanceType, 0, len(instanceTypes)) + for _, instanceType := range instanceTypes { + for _, architecture := range instanceType.SupportedArchitectures { + if filter.IsAllowed(architecture) { + filtered = append(filtered, instanceType) + break + } + } + } + return filtered +} +``` + +Do not add location or instance-name filtering in this task. + +- [ ] **Step 5: Format and run the complete FluidStack package** + +Run: + +```bash +gofmt -w v1/providers/fluidstack/instancetype.go v1/providers/fluidstack/instancetype_test.go +go test ./v1/providers/fluidstack -count=1 +``` + +Expected: all FluidStack tests pass. + +- [ ] **Step 6: Commit the provider change** + +```bash +git add v1/providers/fluidstack/instancetype.go v1/providers/fluidstack/instancetype_test.go +git commit -m "feat: expose FluidStack instance architectures" +``` + +--- + +### Task 6: Replace Nebius name inference with explicit platform metadata + +**Files:** + +- Modify: `v1/providers/nebius/instancetype.go` +- Create: `v1/providers/nebius/instancetype_test.go` + +- [ ] **Step 1: Write failing platform-map and filter tests** + +Create `v1/providers/nebius/instancetype_test.go`: + +```go +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestNebiusPlatformArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + platform string + want cloudv1.Architecture + }{ + {platform: "gpu-b300-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-b200-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-b200-sxm-a", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-h200-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-h100-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-rtx6000", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-l40s-a", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-l40s-d", want: cloudv1.ArchitectureX86_64}, + {platform: "cpu-d3", want: cloudv1.ArchitectureX86_64}, + {platform: "cpu-e2", want: cloudv1.ArchitectureX86_64}, + {platform: "future-platform", want: cloudv1.ArchitectureUnknown}, + } + + for _, tt := range tests { + t.Run(tt.platform, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, nebiusPlatformArchitecture(tt.platform)) + }) + } +} + +func TestApplyInstanceTypeFiltersUsesArchitectureMetadata(t *testing.T) { + t.Parallel() + + x86Type := cloudv1.InstanceType{ + Type: "x86-type", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + } + armType := cloudv1.InstanceType{ + Type: "arm-type", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + } + unknownType := cloudv1.InstanceType{ + Type: "unknown-type", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureUnknown}, + } + all := []cloudv1.InstanceType{x86Type, armType, unknownType} + + tests := []struct { + name string + args cloudv1.GetInstanceTypeArgs + want []cloudv1.InstanceType + }{ + {name: "unfiltered", want: all}, + { + name: "include x86", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureX86_64, + }, + }, + }, + want: []cloudv1.InstanceType{x86Type}, + }, + { + name: "include ARM", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + want: []cloudv1.InstanceType{armType}, + }, + { + name: "exclude ARM", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + want: []cloudv1.InstanceType{x86Type, unknownType}, + }, + { + name: "exclude x86", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureX86_64, + }, + }, + }, + want: []cloudv1.InstanceType{armType, unknownType}, + }, + } + + client := &NebiusClient{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, client.applyInstanceTypeFilters(all, tt.args)) + }) + } +} +``` + +- [ ] **Step 2: Run the focused tests and confirm the red state** + +Run: + +```bash +go test ./v1/providers/nebius -run 'NebiusPlatformArchitecture|ApplyInstanceTypeFiltersUsesArchitectureMetadata' -count=1 +``` + +Expected: `nebiusPlatformArchitecture` is undefined, and exclusion cases expose +the current include-only, type-name-based behavior. + +- [ ] **Step 3: Add the explicit provider-local platform map** + +Add near the existing platform helpers: + +```go +var nebiusPlatformArchitectures = map[string]v1.Architecture{ + "gpu-b300-sxm": v1.ArchitectureX86_64, + "gpu-b200-sxm": v1.ArchitectureX86_64, + "gpu-b200-sxm-a": v1.ArchitectureX86_64, + "gpu-h200-sxm": v1.ArchitectureX86_64, + "gpu-h100-sxm": v1.ArchitectureX86_64, + "gpu-rtx6000": v1.ArchitectureX86_64, + "gpu-l40s-a": v1.ArchitectureX86_64, + "gpu-l40s-d": v1.ArchitectureX86_64, + "cpu-d3": v1.ArchitectureX86_64, + "cpu-e2": v1.ArchitectureX86_64, +} + +func nebiusPlatformArchitecture(platformName string) v1.Architecture { + architecture, ok := nebiusPlatformArchitectures[ + strings.ToLower(strings.TrimSpace(platformName)) + ] + if !ok { + return v1.ArchitectureUnknown + } + return architecture +} +``` + +Use exact platform IDs rather than GPU-generation inference. This is why B200 +and B300 map to x86. + +- [ ] **Step 4: Populate `SupportedArchitectures` at construction** + +In the `v1.InstanceType` literal inside `getInstanceTypesForLocation`, add: + +```go +SupportedArchitectures: []v1.Architecture{ + nebiusPlatformArchitecture(platform.Metadata.Name), +}, +``` + +- [ ] **Step 5: Replace custom architecture filtering** + +Add: + +```go +func supportsAllowedArchitecture( + instanceType v1.InstanceType, + filter *v1.ArchitectureFilter, +) bool { + for _, architecture := range instanceType.SupportedArchitectures { + if filter.IsAllowed(architecture) { + return true + } + } + return false +} +``` + +Replace the current architecture block in `applyInstanceTypeFilters` with: + +```go +if args.ArchitectureFilter != nil && + !supportsAllowedArchitecture(instanceType, args.ArchitectureFilter) { + continue +} +``` + +Delete `determineInstanceTypeArchitecture`. Do not add a synthetic ARM platform. + +- [ ] **Step 6: Format and run the complete Nebius package** + +Run: + +```bash +gofmt -w v1/providers/nebius/instancetype.go v1/providers/nebius/instancetype_test.go +go test ./v1/providers/nebius -count=1 +``` + +Expected: all Nebius tests pass. Unknown platforms remain present in an +unfiltered result and are handled according to the caller's filter. + +- [ ] **Step 7: Commit the provider change** + +```bash +git add v1/providers/nebius/instancetype.go v1/providers/nebius/instancetype_test.go +git commit -m "feat: report Nebius platform architectures" +``` + +--- + +### Task 7: Make the Nebius image catalog architecture-complete + +**Files:** + +- Modify: `v1/providers/nebius/image.go` +- Create: `v1/providers/nebius/image_test.go` + +- [ ] **Step 1: Write failing extraction and unfiltered-list tests** + +Create `v1/providers/nebius/image_test.go`: + +```go +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + common "github.com/nebius/gosdk/proto/nebius/common/v1" + compute "github.com/nebius/gosdk/proto/nebius/compute/v1" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestExtractArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + image *compute.Image + want string + }{ + { + name: "authoritative AMD64 enum", + image: &compute.Image{ + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_AMD64, + }, + }, + want: string(cloudv1.ArchitectureX86_64), + }, + { + name: "authoritative ARM64 enum", + image: &compute.Image{ + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_ARM64, + }, + }, + want: string(cloudv1.ArchitectureARM64), + }, + { + name: "enum wins over legacy label", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{ + Labels: map[string]string{"architecture": "amd64"}, + }, + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_ARM64, + }, + }, + want: string(cloudv1.ArchitectureARM64), + }, + { + name: "legacy label is canonicalized", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{ + Labels: map[string]string{"arch": "aarch64"}, + }, + }, + want: string(cloudv1.ArchitectureARM64), + }, + { + name: "legacy name fallback", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{Name: "ubuntu-24.04-amd64"}, + }, + want: string(cloudv1.ArchitectureX86_64), + }, + { + name: "unknown stays unknown", + image: &compute.Image{}, + want: string(cloudv1.ArchitectureUnknown), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, extractArchitecture(tt.image)) + }) + } +} + +func TestApplyImageFiltersWithoutArchitectureReturnsAll(t *testing.T) { + t.Parallel() + + images := []cloudv1.Image{ + {ID: "amd64", Architecture: string(cloudv1.ArchitectureX86_64)}, + {ID: "arm64", Architecture: string(cloudv1.ArchitectureARM64)}, + {ID: "unknown", Architecture: string(cloudv1.ArchitectureUnknown)}, + } + + require.Equal( + t, + images, + applyImageFilters(images, cloudv1.GetImageArgs{}), + ) +} +``` + +- [ ] **Step 2: Run the focused tests and confirm the red state** + +Run: + +```bash +go test ./v1/providers/nebius -run 'ExtractArchitecture|ApplyImageFilters' -count=1 +``` + +Expected: the authoritative enum and unknown cases fail, and +`applyImageFilters` is undefined. + +- [ ] **Step 3: Make the provider enum authoritative** + +Replace `extractArchitecture` with: + +```go +func extractArchitecture(image *compute.Image) string { + if image != nil && image.Spec != nil { + switch image.Spec.CpuArchitecture { + case compute.ImageSpec_AMD64: + return string(v1.ArchitectureX86_64) + case compute.ImageSpec_ARM64: + return string(v1.ArchitectureARM64) + } + } + + if image != nil && image.Metadata != nil { + for _, label := range []string{"architecture", "arch"} { + rawArchitecture, ok := image.Metadata.Labels[label] + if !ok { + continue + } + architecture := v1.GetArchitecture(rawArchitecture) + if architecture != v1.ArchitectureUnknown { + return string(architecture) + } + } + + name := strings.ToLower(image.Metadata.Name) + if strings.Contains(name, ArchitectureArm64) || + strings.Contains(name, ArchitectureAArch64) { + return string(v1.ArchitectureARM64) + } + if strings.Contains(name, ArchitectureX86_64) || + strings.Contains(name, ArchitectureAMD64) { + return string(v1.ArchitectureX86_64) + } + } + + return string(v1.ArchitectureUnknown) +} +``` + +- [ ] **Step 4: Centralize and remove the default x86 filter** + +Add: + +```go +func applyImageFilters( + images []v1.Image, + args v1.GetImageArgs, +) []v1.Image { + images = filterImagesByArchitectures(images, args.Architectures) + return filterImagesByNameFilters(images, args.NameFilters) +} +``` + +In `GetImages`, delete the block that substitutes `[]string{"x86_64"}` when +`args.Architectures` is empty. Replace both filter blocks with: + +```go +return applyImageFilters(images, args), nil +``` + +In `getDefaultImages`, replace the hardcoded architecture with: + +```go +Architecture: extractArchitecture(image), +``` + +- [ ] **Step 5: Format and run the complete Nebius package** + +Run: + +```bash +gofmt -w v1/providers/nebius/image.go v1/providers/nebius/image_test.go +go test ./v1/providers/nebius -count=1 +``` + +Expected: all Nebius tests pass, and an empty architecture filter preserves +AMD64, ARM64, and unknown images. + +- [ ] **Step 6: Commit the image-catalog change** + +```bash +git add v1/providers/nebius/image.go v1/providers/nebius/image_test.go +git commit -m "feat: expose Nebius image architectures" +``` + +--- + +### Task 8: Record SFCompute's current x86-only provider contract + +**Files:** + +- Create: `v1/providers/sfcompute/instancetype_test.go` +- Create: `v1/providers/sfcomputev2/instancetype_test.go` +- Verify only: `v1/providers/sfcompute/instancetype.go` +- Verify only: `v1/providers/sfcomputev2/instancetype.go` + +- [ ] **Step 1: Add SFCompute v1 characterization coverage** + +Create `v1/providers/sfcompute/instancetype_test.go`: + +```go +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestSFComputeInstanceTypeArchitectures(t *testing.T) { + t.Parallel() + + for _, gpuType := range []string{gpuTypeH100, gpuTypeH200} { + t.Run(gpuType, func(t *testing.T) { + t.Parallel() + metadata, err := getInstanceTypeMetadata(gpuType) + require.NoError(t, err) + require.Equal(t, cloudv1.ArchitectureX86_64, metadata.architecture) + }) + } +} +``` + +- [ ] **Step 2: Add SFCompute v2 characterization and filter coverage** + +Create `v1/providers/sfcomputev2/instancetype_test.go`: + +```go +package v2 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestSFComputeV2InstanceTypeArchitecture(t *testing.T) { + t.Parallel() + + instanceType := buildInstanceType(h100InstanceTypeMetadata, true) + require.Equal( + t, + []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + instanceType.SupportedArchitectures, + ) + require.False( + t, + cloudv1.IsSelectedByArgs( + instanceType, + cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + ), + ) +} +``` + +- [ ] **Step 3: Run both characterization packages** + +Run: + +```bash +gofmt -w v1/providers/sfcompute/instancetype_test.go v1/providers/sfcomputev2/instancetype_test.go +go test ./v1/providers/sfcompute ./v1/providers/sfcomputev2 -count=1 +``` + +Expected: tests pass without production changes. H100 and H200 remain truthfully +x86 until SFCompute exposes an authoritative Grace SKU and compatible image. + +- [ ] **Step 4: Commit the contract tests** + +```bash +git add v1/providers/sfcompute/instancetype_test.go v1/providers/sfcomputev2/instancetype_test.go +git commit -m "test: record SFCompute architecture contracts" +``` + +--- + +### Task 9: Run cross-provider verification and inspect the final diff + +**Files:** + +- Verify all files changed by Tasks 1 through 8 + +- [ ] **Step 1: Format every touched Go file** + +Run: + +```bash +gofmt -w \ + v1/instancetype.go \ + v1/instancetype_test.go \ + v1/providers/launchpad/instancetype_test.go \ + v1/providers/shadeform/instancetype.go \ + v1/providers/shadeform/instancetype_test.go \ + v1/providers/lambdalabs/instancetype.go \ + v1/providers/lambdalabs/instancetype_test.go \ + v1/providers/fluidstack/instancetype.go \ + v1/providers/fluidstack/instancetype_test.go \ + v1/providers/nebius/instancetype.go \ + v1/providers/nebius/instancetype_test.go \ + v1/providers/nebius/image.go \ + v1/providers/nebius/image_test.go \ + v1/providers/sfcompute/instancetype_test.go \ + v1/providers/sfcomputev2/instancetype_test.go +``` + +- [ ] **Step 2: Run the focused cross-provider suite** + +Run: + +```bash +go test \ + ./v1 \ + ./v1/providers/launchpad \ + ./v1/providers/shadeform \ + ./v1/providers/lambdalabs \ + ./v1/providers/fluidstack \ + ./v1/providers/nebius \ + ./v1/providers/sfcompute \ + ./v1/providers/sfcomputev2 \ + -count=1 +``` + +Expected: all focused packages pass. + +- [ ] **Step 3: Run repository-wide tests** + +Run: + +```bash +go test ./v1/... -count=1 +go test ./... -count=1 +``` + +Expected: both commands pass. Investigate any failure before claiming completion; +do not dismiss provider failures without reproducing the exact package. + +- [ ] **Step 4: Run formatting and lint checks** + +Run: + +```bash +make fmt-check +golangci-lint run ./... +``` + +Expected: both commands pass. If `golangci-lint` is not installed, use the +repository-pinned version from `Makefile` rather than changing tool versions. + +- [ ] **Step 5: Verify scope and semantics in the diff** + +Run: + +```bash +git diff --check +git status --short +git diff --stat HEAD~8..HEAD +git diff HEAD~8..HEAD -- \ + v1/instancetype.go \ + v1/providers/launchpad \ + v1/providers/shadeform \ + v1/providers/lambdalabs \ + v1/providers/fluidstack \ + v1/providers/nebius \ + v1/providers/sfcompute \ + v1/providers/sfcomputev2 +``` + +Confirm: + +- no dev-plane or Verb files changed; +- no generated clients changed; +- Launchpad still uses `SystemArch`; +- Shadeform, LambdaLabs, and FluidStack share only Grace recognition; +- FluidStack added only architecture filtering; +- Nebius unknown platforms and images remain `unknown`; +- no synthetic Nebius or TestKube ARM offering was introduced; +- ARM provisioning and boot-image selection remain unchanged. + +- [ ] **Step 6: Commit any verification-only formatting adjustment** + +Only if Step 1 changed a file after its task commit: + +```bash +git add v1 +git commit -m "style: format ARM catalog changes" +``` + +If `git status --short` is empty, do not create an empty commit. From 0921aa7c939d011f648c15a3d5d31df9b1db52c9 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:09:00 -0700 Subject: [PATCH 03/17] feat: classify NVIDIA Grace GPU models --- v1/instancetype.go | 7 +++++++ v1/instancetype_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/v1/instancetype.go b/v1/instancetype.go index a58bc0b..2dead39 100644 --- a/v1/instancetype.go +++ b/v1/instancetype.go @@ -52,6 +52,13 @@ func GetArchitecture(architecture string) Architecture { } } +func IsNVIDIAGraceGPU(gpuModel string) bool { + normalizedModel := strings.ToUpper(strings.TrimSpace(gpuModel)) + normalizedModel = strings.TrimPrefix(normalizedModel, "NVIDIA ") + return strings.HasPrefix(normalizedModel, "GH") || + strings.HasPrefix(normalizedModel, "GB") +} + type InstanceTypeID string type InstanceType struct { diff --git a/v1/instancetype_test.go b/v1/instancetype_test.go index d76b323..4e43d23 100644 --- a/v1/instancetype_test.go +++ b/v1/instancetype_test.go @@ -28,3 +28,31 @@ func TestGetArchitectureAliases(t *testing.T) { }) } } + +func TestIsNVIDIAGraceGPU(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + gpuModel string + want bool + }{ + {name: "GH200", gpuModel: "GH200", want: true}, + {name: "GB200", gpuModel: "GB200", want: true}, + {name: "lowercase GH200", gpuModel: "gh200", want: true}, + {name: "trimmed GB200", gpuModel: " gb200 ", want: true}, + {name: "vendor-prefixed GH200", gpuModel: "NVIDIA GH200", want: true}, + {name: "H100", gpuModel: "H100", want: false}, + {name: "B200", gpuModel: "B200", want: false}, + {name: "empty", gpuModel: "", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := IsNVIDIAGraceGPU(tt.gpuModel); got != tt.want { + t.Fatalf("IsNVIDIAGraceGPU(%q) = %v, want %v", tt.gpuModel, got, tt.want) + } + }) + } +} From 2d58936f7c11250eba595bbca7fae02a9366f553 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:12:21 -0700 Subject: [PATCH 04/17] test: lock Launchpad architecture mapping --- v1/providers/launchpad/instancetype_test.go | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/v1/providers/launchpad/instancetype_test.go b/v1/providers/launchpad/instancetype_test.go index 43822f3..a959d4d 100644 --- a/v1/providers/launchpad/instancetype_test.go +++ b/v1/providers/launchpad/instancetype_test.go @@ -8,6 +8,7 @@ import ( "github.com/brevdev/cloud/internal/validation" v1 "github.com/brevdev/cloud/v1" + openapi "github.com/brevdev/cloud/v1/providers/launchpad/gen/launchpad" ) func TestGetInstanceTypes(t *testing.T) { @@ -130,3 +131,41 @@ func TestMakeGenericInstanceTypeID(t *testing.T) { }) } } + +func TestLaunchpadArchitectureToArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + raw openapi.SystemArchEnum + want v1.Architecture + }{ + {name: "AMD64", raw: openapi.SystemArchAMD64, want: v1.ArchitectureX86_64}, + {name: "ARM64", raw: openapi.SystemArchARM64, want: v1.ArchitectureARM64}, + {name: "unknown", raw: openapi.SystemArchEnum("riscv64"), want: v1.ArchitectureUnknown}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, launchpadArchitectureToArchitecture(tt.raw)) + }) + } +} + +func TestLaunchpadArchitectureFilterExcludesARM(t *testing.T) { + t.Parallel() + + instanceType := v1.InstanceType{ + SupportedArchitectures: []v1.Architecture{ + launchpadArchitectureToArchitecture(openapi.SystemArchARM64), + }, + } + args := v1.GetInstanceTypeArgs{ + ArchitectureFilter: &v1.ArchitectureFilter{ + ExcludeArchitectures: []v1.Architecture{v1.ArchitectureARM64}, + }, + } + + require.False(t, v1.IsSelectedByArgs(instanceType, args)) +} From b688540d89372bdcf54bf88b029a7dde42441ed4 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:15:06 -0700 Subject: [PATCH 05/17] feat: normalize Shadeform Grace architectures --- v1/providers/shadeform/instancetype.go | 3 +-- v1/providers/shadeform/instancetype_test.go | 23 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/v1/providers/shadeform/instancetype.go b/v1/providers/shadeform/instancetype.go index 06b0701..3cd32a8 100644 --- a/v1/providers/shadeform/instancetype.go +++ b/v1/providers/shadeform/instancetype.go @@ -287,8 +287,7 @@ func shadeformCloud(cloud string) string { } func shadeformArchitecture(gpuName string) v1.Architecture { - // Shadeform currently does not specify the architecture, so we need to infer it from the GPU name. - if strings.HasPrefix(gpuName, "GH") || strings.HasPrefix(gpuName, "GB") { + if v1.IsNVIDIAGraceGPU(gpuName) { return v1.ArchitectureARM64 } return v1.ArchitectureX86_64 diff --git a/v1/providers/shadeform/instancetype_test.go b/v1/providers/shadeform/instancetype_test.go index 27e074b..c230304 100644 --- a/v1/providers/shadeform/instancetype_test.go +++ b/v1/providers/shadeform/instancetype_test.go @@ -9,6 +9,29 @@ import ( "github.com/stretchr/testify/assert" ) +func TestShadeformArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + gpuModel string + want v1.Architecture + }{ + {gpuModel: "GH200", want: v1.ArchitectureARM64}, + {gpuModel: "GB200", want: v1.ArchitectureARM64}, + {gpuModel: "gh200", want: v1.ArchitectureARM64}, + {gpuModel: "gb200", want: v1.ArchitectureARM64}, + {gpuModel: "H100", want: v1.ArchitectureX86_64}, + {gpuModel: "B200", want: v1.ArchitectureX86_64}, + } + + for _, tt := range tests { + t.Run(tt.gpuModel, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, shadeformArchitecture(tt.gpuModel)) + }) + } +} + func TestIsSelectedByArgs(t *testing.T) { t.Parallel() From 78400c610145cfef441082c9cc424b03bb2cdc16 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:21:29 -0700 Subject: [PATCH 06/17] feat: expose FluidStack instance architectures --- v1/providers/fluidstack/instancetype.go | 51 ++++++++++--- v1/providers/fluidstack/instancetype_test.go | 78 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 v1/providers/fluidstack/instancetype_test.go diff --git a/v1/providers/fluidstack/instancetype.go b/v1/providers/fluidstack/instancetype.go index a677966..58b207d 100644 --- a/v1/providers/fluidstack/instancetype.go +++ b/v1/providers/fluidstack/instancetype.go @@ -13,7 +13,7 @@ import ( openapi "github.com/brevdev/cloud/v1/providers/fluidstack/gen/fluidstack" ) -func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, _ v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { +func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { authCtx := c.makeAuthContext(ctx) projectCtx := c.makeProjectContext(authCtx) @@ -31,7 +31,27 @@ func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, _ v1.GetInstanc instanceTypes = append(instanceTypes, instanceType) } - return instanceTypes, nil + return filterInstanceTypesByArchitecture(instanceTypes, args.ArchitectureFilter), nil +} + +func filterInstanceTypesByArchitecture( + instanceTypes []v1.InstanceType, + filter *v1.ArchitectureFilter, +) []v1.InstanceType { + if filter == nil { + return instanceTypes + } + + filtered := make([]v1.InstanceType, 0, len(instanceTypes)) + for _, instanceType := range instanceTypes { + for _, architecture := range instanceType.SupportedArchitectures { + if filter.IsAllowed(architecture) { + filtered = append(filtered, instanceType) + break + } + } + } + return filtered } func (c *FluidStackClient) GetInstanceTypePollTime() time.Duration { @@ -100,16 +120,23 @@ func convertFluidStackInstanceTypeToV1InstanceType(location string, fsInstanceTy price, _ := currency.NewAmount("0", "USD") + architecture := v1.ArchitectureX86_64 + if fsInstanceType.GpuModel != nil && + v1.IsNVIDIAGraceGPU(*fsInstanceType.GpuModel) { + architecture = v1.ArchitectureARM64 + } + return v1.InstanceType{ - Type: fsInstanceType.Name, - VCPU: vcpus, - Memory: ram, - MemoryBytes: memoryBytes, - SupportedGPUs: gpus, - BasePrice: &price, - IsAvailable: isAvailable, - Location: location, - Provider: CloudProviderID, - Cloud: CloudProviderID, + Type: fsInstanceType.Name, + VCPU: vcpus, + Memory: ram, + MemoryBytes: memoryBytes, + SupportedGPUs: gpus, + SupportedArchitectures: []v1.Architecture{architecture}, + BasePrice: &price, + IsAvailable: isAvailable, + Location: location, + Provider: CloudProviderID, + Cloud: CloudProviderID, } } diff --git a/v1/providers/fluidstack/instancetype_test.go b/v1/providers/fluidstack/instancetype_test.go new file mode 100644 index 0000000..6de2192 --- /dev/null +++ b/v1/providers/fluidstack/instancetype_test.go @@ -0,0 +1,78 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" + openapi "github.com/brevdev/cloud/v1/providers/fluidstack/gen/fluidstack" +) + +func TestConvertFluidStackInstanceTypeArchitecture(t *testing.T) { + t.Parallel() + + gh200 := "gh200" + b200 := "B200" + tests := []struct { + name string + gpuModel *string + want cloudv1.Architecture + }{ + {name: "GH200", gpuModel: &gh200, want: cloudv1.ArchitectureARM64}, + {name: "B200", gpuModel: &b200, want: cloudv1.ArchitectureX86_64}, + {name: "CPU", gpuModel: nil, want: cloudv1.ArchitectureX86_64}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gpuCount := int32(1) + if tt.gpuModel == nil { + gpuCount = 0 + } + got := convertFluidStackInstanceTypeToV1InstanceType( + "", + openapi.InstanceType{ + Name: "test-" + tt.name, + Cpu: 64, + Memory: "432GB", + GpuModel: tt.gpuModel, + GpuCount: &gpuCount, + }, + true, + ) + require.Equal(t, []cloudv1.Architecture{tt.want}, got.SupportedArchitectures) + }) + } +} + +func TestFilterFluidStackInstanceTypesByArchitecture(t *testing.T) { + t.Parallel() + + x86Type := cloudv1.InstanceType{ + Type: "x86", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + } + armType := cloudv1.InstanceType{ + Type: "arm", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + } + instanceTypes := []cloudv1.InstanceType{x86Type, armType} + + require.Equal( + t, + instanceTypes, + filterInstanceTypesByArchitecture(instanceTypes, nil), + ) + require.Equal( + t, + []cloudv1.InstanceType{x86Type}, + filterInstanceTypesByArchitecture( + instanceTypes, + &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + }, + ), + ) +} From 5656e320388efb35a620f2abe75618fa4df15b6d Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:25:25 -0700 Subject: [PATCH 07/17] feat: report Nebius platform architectures --- v1/providers/nebius/instancetype.go | 83 ++++++++-------- v1/providers/nebius/instancetype_test.go | 115 +++++++++++++++++++++++ 2 files changed, 160 insertions(+), 38 deletions(-) create mode 100644 v1/providers/nebius/instancetype_test.go diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index fc8c6bd..97a4f07 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -186,17 +186,18 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform // Convert Nebius platform preset to our InstanceType format instanceType := v1.InstanceType{ - Location: location.Name, - Type: instanceTypeID, // Same as ID - both use dot-separated format - VCPU: preset.Resources.VcpuCount, - Memory: units.Base2Bytes(preset.Resources.MemoryGibibytes) * units.Gibibyte, - MemoryBytes: v1.NewBytes(v1.BytesValue(preset.Resources.MemoryGibibytes), v1.Gibibyte), // Memory in GiB - NetworkPerformance: "standard", // Default network performance - IsAvailable: isAvailable, - Stoppable: true, // All Nebius instances support stop/start operations - ElasticRootVolume: true, // Nebius supports dynamic disk allocation - SupportedStorage: c.buildSupportedStorage(), - Provider: CloudProviderID, // Nebius is the provider + Location: location.Name, + Type: instanceTypeID, // Same as ID - both use dot-separated format + VCPU: preset.Resources.VcpuCount, + Memory: units.Base2Bytes(preset.Resources.MemoryGibibytes) * units.Gibibyte, + MemoryBytes: v1.NewBytes(v1.BytesValue(preset.Resources.MemoryGibibytes), v1.Gibibyte), // Memory in GiB + NetworkPerformance: "standard", // Default network performance + IsAvailable: isAvailable, + Stoppable: true, // All Nebius instances support stop/start operations + ElasticRootVolume: true, // Nebius supports dynamic disk allocation + SupportedStorage: c.buildSupportedStorage(), + SupportedArchitectures: []v1.Architecture{nebiusPlatformArchitecture(platform.Metadata.Name)}, + Provider: CloudProviderID, // Nebius is the provider } // Add GPU information if available @@ -439,6 +440,27 @@ func (c *NebiusClient) getGPUQuotaName(platformName string) string { return "" } +var nebiusPlatformArchitectures = map[string]v1.Architecture{ + "gpu-b300-sxm": v1.ArchitectureX86_64, + "gpu-b200-sxm": v1.ArchitectureX86_64, + "gpu-b200-sxm-a": v1.ArchitectureX86_64, + "gpu-h200-sxm": v1.ArchitectureX86_64, + "gpu-h100-sxm": v1.ArchitectureX86_64, + "gpu-rtx6000": v1.ArchitectureX86_64, + "gpu-l40s-a": v1.ArchitectureX86_64, + "gpu-l40s-d": v1.ArchitectureX86_64, + "cpu-d3": v1.ArchitectureX86_64, + "cpu-e2": v1.ArchitectureX86_64, +} + +func nebiusPlatformArchitecture(platformName string) v1.Architecture { + architecture, ok := nebiusPlatformArchitectures[strings.ToLower(strings.TrimSpace(platformName))] + if !ok { + return v1.ArchitectureUnknown + } + return architecture +} + // isPlatformSupported checks if a platform should be included in instance types func (c *NebiusClient) isPlatformSupported(platformName string) bool { platformLower := strings.ToLower(platformName) @@ -513,22 +535,9 @@ func (c *NebiusClient) applyInstanceTypeFilters(instanceTypes []v1.InstanceType, } } - // Apply architecture filter - if args.ArchitectureFilter != nil { - arch := determineInstanceTypeArchitecture(instanceType) - // Check if architecture matches the filter requirements - if len(args.ArchitectureFilter.IncludeArchitectures) > 0 { - found := false - for _, allowedArch := range args.ArchitectureFilter.IncludeArchitectures { - if arch == string(allowedArch) { - found = true - break - } - } - if !found { - continue - } - } + if args.ArchitectureFilter != nil && + !supportsAllowedArchitecture(instanceType, args.ArchitectureFilter) { + continue } filtered = append(filtered, instanceType) @@ -537,6 +546,15 @@ func (c *NebiusClient) applyInstanceTypeFilters(instanceTypes []v1.InstanceType, return filtered } +func supportsAllowedArchitecture(instanceType v1.InstanceType, filter *v1.ArchitectureFilter) bool { + for _, architecture := range instanceType.SupportedArchitectures { + if filter.IsAllowed(architecture) { + return true + } + } + return false +} + // extractGPUTypeAndName extracts GPU type and name from platform name // Note: Returns model name only (e.g., "H100"), not full name with manufacturer // Manufacturer info is stored separately in GPU.Manufacturer field @@ -588,17 +606,6 @@ func getGPUMemory(gpuType string) units.Base2Bytes { return units.Base2Bytes(0) } -// determineInstanceTypeArchitecture determines architecture from instance type -func determineInstanceTypeArchitecture(instanceType v1.InstanceType) string { - // Check if ARM architecture is indicated in the type or name - typeLower := strings.ToLower(instanceType.Type) - if strings.Contains(typeLower, "arm") || strings.Contains(typeLower, "aarch64") { - return "arm64" - } - - return "x86_64" // Default assumption -} - // getPricingForInstanceType fetches real pricing from Nebius Billing Calculator API // Returns nil if pricing cannot be fetched (non-critical failure) func (c *NebiusClient) getPricingForInstanceType(ctx context.Context, platformName, presetName, _ string) *currency.Amount { diff --git a/v1/providers/nebius/instancetype_test.go b/v1/providers/nebius/instancetype_test.go new file mode 100644 index 0000000..5500a43 --- /dev/null +++ b/v1/providers/nebius/instancetype_test.go @@ -0,0 +1,115 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestNebiusPlatformArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + platform string + want cloudv1.Architecture + }{ + {platform: "gpu-b300-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-b200-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-b200-sxm-a", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-h200-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-h100-sxm", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-rtx6000", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-l40s-a", want: cloudv1.ArchitectureX86_64}, + {platform: "gpu-l40s-d", want: cloudv1.ArchitectureX86_64}, + {platform: "cpu-d3", want: cloudv1.ArchitectureX86_64}, + {platform: "cpu-e2", want: cloudv1.ArchitectureX86_64}, + {platform: "future-platform", want: cloudv1.ArchitectureUnknown}, + } + + for _, tt := range tests { + t.Run(tt.platform, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, nebiusPlatformArchitecture(tt.platform)) + }) + } +} + +func TestApplyInstanceTypeFiltersUsesArchitectureMetadata(t *testing.T) { + t.Parallel() + + x86Type := cloudv1.InstanceType{ + Type: "x86-type", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + } + armType := cloudv1.InstanceType{ + Type: "arm-type", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + } + unknownType := cloudv1.InstanceType{ + Type: "unknown-type", + SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureUnknown}, + } + all := []cloudv1.InstanceType{x86Type, armType, unknownType} + + tests := []struct { + name string + args cloudv1.GetInstanceTypeArgs + want []cloudv1.InstanceType + }{ + {name: "unfiltered", want: all}, + { + name: "include x86", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureX86_64, + }, + }, + }, + want: []cloudv1.InstanceType{x86Type}, + }, + { + name: "include ARM", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + want: []cloudv1.InstanceType{armType}, + }, + { + name: "exclude ARM", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + want: []cloudv1.InstanceType{x86Type, unknownType}, + }, + { + name: "exclude x86", + args: cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureX86_64, + }, + }, + }, + want: []cloudv1.InstanceType{armType, unknownType}, + }, + } + + client := &NebiusClient{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, client.applyInstanceTypeFilters(all, tt.args)) + }) + } +} From 634f65800e317a317531064c102b65f06b86b680 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:30:00 -0700 Subject: [PATCH 08/17] feat: expose Nebius image architectures --- v1/providers/nebius/image.go | 56 +++++++++--------- v1/providers/nebius/image_test.go | 96 +++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 v1/providers/nebius/image_test.go diff --git a/v1/providers/nebius/image.go b/v1/providers/nebius/image.go index 966ade1..caf564a 100644 --- a/v1/providers/nebius/image.go +++ b/v1/providers/nebius/image.go @@ -32,19 +32,7 @@ func (c *NebiusClient) GetImages(ctx context.Context, args v1.GetImageArgs) ([]v } } - // Apply architecture filters - default to x86_64 if no architecture specified - architectures := args.Architectures - if len(architectures) == 0 { - architectures = []string{"x86_64"} // Default to x86_64 - } - images = filterImagesByArchitectures(images, architectures) - - // Apply name filter if specified - if len(args.NameFilters) > 0 { - images = filterImagesByNameFilters(images, args.NameFilters) - } - - return images, nil + return applyImageFilters(images, args), nil } // getProjectImages retrieves images specific to the current project @@ -180,7 +168,7 @@ func (c *NebiusClient) getDefaultImages(ctx context.Context) ([]v1.Image, error) ID: image.Metadata.Id, Name: image.Metadata.Name, Description: getImageDescription(image), - Architecture: "x86_64", + Architecture: extractArchitecture(image), } // Set creation time if available @@ -209,30 +197,44 @@ const ( ArchitectureAArch64 = "aarch64" ) -// extractArchitecture extracts architecture information from image metadata +// extractArchitecture extracts architecture information from image metadata. func extractArchitecture(image *compute.Image) string { - // Check labels for architecture info - if image.Metadata != nil && image.Metadata.Labels != nil { - if arch, exists := image.Metadata.Labels["architecture"]; exists { - return arch - } - if arch, exists := image.Metadata.Labels["arch"]; exists { - return arch + if image != nil && image.Spec != nil { + switch image.Spec.CpuArchitecture { + case compute.ImageSpec_AMD64: + return string(v1.ArchitectureX86_64) + case compute.ImageSpec_ARM64: + return string(v1.ArchitectureARM64) } } - // Infer from image name - if image.Metadata != nil { + if image != nil && image.Metadata != nil { + for _, label := range []string{"architecture", "arch"} { + rawArchitecture, ok := image.Metadata.Labels[label] + if !ok { + continue + } + architecture := v1.GetArchitecture(rawArchitecture) + if architecture != v1.ArchitectureUnknown { + return string(architecture) + } + } + name := strings.ToLower(image.Metadata.Name) if strings.Contains(name, ArchitectureArm64) || strings.Contains(name, ArchitectureAArch64) { - return ArchitectureArm64 + return string(v1.ArchitectureARM64) } if strings.Contains(name, ArchitectureX86_64) || strings.Contains(name, ArchitectureAMD64) { - return ArchitectureX86_64 + return string(v1.ArchitectureX86_64) } } - return ArchitectureX86_64 + return string(v1.ArchitectureUnknown) +} + +func applyImageFilters(images []v1.Image, args v1.GetImageArgs) []v1.Image { + images = filterImagesByArchitectures(images, args.Architectures) + return filterImagesByNameFilters(images, args.NameFilters) } // filterImagesByArchitectures filters images by multiple architectures diff --git a/v1/providers/nebius/image_test.go b/v1/providers/nebius/image_test.go new file mode 100644 index 0000000..5df26d3 --- /dev/null +++ b/v1/providers/nebius/image_test.go @@ -0,0 +1,96 @@ +package v1 + +import ( + "testing" + + common "github.com/nebius/gosdk/proto/nebius/common/v1" + compute "github.com/nebius/gosdk/proto/nebius/compute/v1" + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestExtractArchitecture(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + image *compute.Image + want string + }{ + { + name: "authoritative AMD64 enum", + image: &compute.Image{ + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_AMD64, + }, + }, + want: string(cloudv1.ArchitectureX86_64), + }, + { + name: "authoritative ARM64 enum", + image: &compute.Image{ + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_ARM64, + }, + }, + want: string(cloudv1.ArchitectureARM64), + }, + { + name: "enum wins over legacy label", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{ + Labels: map[string]string{"architecture": "amd64"}, + }, + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_ARM64, + }, + }, + want: string(cloudv1.ArchitectureARM64), + }, + { + name: "legacy label is canonicalized", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{ + Labels: map[string]string{"arch": "aarch64"}, + }, + }, + want: string(cloudv1.ArchitectureARM64), + }, + { + name: "legacy name fallback", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{Name: "ubuntu-24.04-amd64"}, + }, + want: string(cloudv1.ArchitectureX86_64), + }, + { + name: "unknown stays unknown", + image: &compute.Image{}, + want: string(cloudv1.ArchitectureUnknown), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, extractArchitecture(tt.image)) + }) + } +} + +func TestApplyImageFiltersWithoutArchitectureReturnsAll(t *testing.T) { + t.Parallel() + + images := []cloudv1.Image{ + {ID: "amd64", Architecture: string(cloudv1.ArchitectureX86_64)}, + {ID: "arm64", Architecture: string(cloudv1.ArchitectureARM64)}, + {ID: "unknown", Architecture: string(cloudv1.ArchitectureUnknown)}, + } + + require.Equal( + t, + images, + applyImageFilters(images, cloudv1.GetImageArgs{}), + ) +} From 691bc44f81b9436ef0f046d614b4a20d242dd704 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:33:55 -0700 Subject: [PATCH 09/17] test: record SFCompute architecture contracts --- v1/providers/sfcompute/instancetype_test.go | 22 +++++++++++++ v1/providers/sfcomputev2/instancetype_test.go | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 v1/providers/sfcompute/instancetype_test.go create mode 100644 v1/providers/sfcomputev2/instancetype_test.go diff --git a/v1/providers/sfcompute/instancetype_test.go b/v1/providers/sfcompute/instancetype_test.go new file mode 100644 index 0000000..fc83dd2 --- /dev/null +++ b/v1/providers/sfcompute/instancetype_test.go @@ -0,0 +1,22 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestSFComputeInstanceTypeArchitectures(t *testing.T) { + t.Parallel() + + for _, gpuType := range []string{gpuTypeH100, gpuTypeH200} { + t.Run(gpuType, func(t *testing.T) { + t.Parallel() + metadata, err := getInstanceTypeMetadata(gpuType) + require.NoError(t, err) + require.Equal(t, cloudv1.ArchitectureX86_64, metadata.architecture) + }) + } +} diff --git a/v1/providers/sfcomputev2/instancetype_test.go b/v1/providers/sfcomputev2/instancetype_test.go new file mode 100644 index 0000000..7c5050a --- /dev/null +++ b/v1/providers/sfcomputev2/instancetype_test.go @@ -0,0 +1,33 @@ +package v2 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + cloudv1 "github.com/brevdev/cloud/v1" +) + +func TestSFComputeV2InstanceTypeArchitecture(t *testing.T) { + t.Parallel() + + instanceType := buildInstanceType(h100InstanceTypeMetadata, true) + require.Equal( + t, + []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + instanceType.SupportedArchitectures, + ) + require.False( + t, + cloudv1.IsSelectedByArgs( + instanceType, + cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + ), + ) +} From 161c6d892fa4f0c9b6988c819527b86fe313fff3 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 15:43:29 -0700 Subject: [PATCH 10/17] chore: remove stale Nebius lint suppression --- v1/providers/nebius/instancetype.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index 97a4f07..cf9ff7b 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -515,8 +515,6 @@ func (c *NebiusClient) buildSupportedStorage() []v1.Storage { } // applyInstanceTypeFilters applies various filters to the instance type list -// -//nolint:gocognit // Complex function with multiple filter conditions for instance types func (c *NebiusClient) applyInstanceTypeFilters(instanceTypes []v1.InstanceType, args v1.GetInstanceTypeArgs) []v1.InstanceType { var filtered []v1.InstanceType From f01201bd58ca0f3a9fdbfb4aa169c40e678450d0 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Mon, 27 Jul 2026 16:09:19 -0700 Subject: [PATCH 11/17] fix: close ARM catalog provider gaps --- v1/providers/fluidstack/client.go | 2 + v1/providers/fluidstack/instancetype.go | 19 +++- v1/providers/fluidstack/instancetype_test.go | 58 ++++++++++ v1/providers/nebius/client.go | 3 + v1/providers/nebius/image.go | 15 ++- v1/providers/nebius/image_test.go | 70 ++++++++++++ v1/providers/nebius/instance_test.go | 49 ++++++--- v1/providers/nebius/instancetype.go | 71 +++++++----- v1/providers/nebius/instancetype_test.go | 108 +++++++++++++++++++ 9 files changed, 350 insertions(+), 45 deletions(-) diff --git a/v1/providers/fluidstack/client.go b/v1/providers/fluidstack/client.go index 5016d52..4b5b542 100644 --- a/v1/providers/fluidstack/client.go +++ b/v1/providers/fluidstack/client.go @@ -70,6 +70,8 @@ type FluidStackClient struct { baseURL string projectID string client *openapi.APIClient + + listInstanceTypes func(context.Context) ([]openapi.InstanceType, error) } var _ v1.CloudClient = &FluidStackClient{} diff --git a/v1/providers/fluidstack/instancetype.go b/v1/providers/fluidstack/instancetype.go index 58b207d..0393fab 100644 --- a/v1/providers/fluidstack/instancetype.go +++ b/v1/providers/fluidstack/instancetype.go @@ -17,10 +17,11 @@ func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, args v1.GetInst authCtx := c.makeAuthContext(ctx) projectCtx := c.makeProjectContext(authCtx) - resp, httpResp, err := c.client.InstanceTypesAPI.ListInstanceTypes(projectCtx).Execute() - if httpResp != nil && httpResp.Body != nil { - defer func() { _ = httpResp.Body.Close() }() + listInstanceTypes := c.listInstanceTypes + if listInstanceTypes == nil { + listInstanceTypes = c.listInstanceTypesFromAPI } + resp, err := listInstanceTypes(projectCtx) if err != nil { return nil, fmt.Errorf("failed to get instance types: %w", err) } @@ -34,6 +35,18 @@ func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, args v1.GetInst return filterInstanceTypesByArchitecture(instanceTypes, args.ArchitectureFilter), nil } +func (c *FluidStackClient) listInstanceTypesFromAPI(ctx context.Context) ([]openapi.InstanceType, error) { + resp, httpResp, err := c.client.InstanceTypesAPI.ListInstanceTypes(ctx).Execute() + if httpResp != nil && httpResp.Body != nil { + defer func() { _ = httpResp.Body.Close() }() + } + if err != nil { + return nil, err + } + + return resp, nil +} + func filterInstanceTypesByArchitecture( instanceTypes []v1.InstanceType, filter *v1.ArchitectureFilter, diff --git a/v1/providers/fluidstack/instancetype_test.go b/v1/providers/fluidstack/instancetype_test.go index 6de2192..1fc64ac 100644 --- a/v1/providers/fluidstack/instancetype_test.go +++ b/v1/providers/fluidstack/instancetype_test.go @@ -1,6 +1,7 @@ package v1 import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -76,3 +77,60 @@ func TestFilterFluidStackInstanceTypesByArchitecture(t *testing.T) { ), ) } + +func TestFluidStackClientGetInstanceTypesAppliesArchitectureFilter(t *testing.T) { + t.Parallel() + + gh200 := "GH200" + h100 := "H100" + gpuCount := int32(1) + catalog := []openapi.InstanceType{ + { + Name: "gpu-gh200", + Cpu: 64, + Memory: "432GB", + GpuModel: &gh200, + GpuCount: &gpuCount, + }, + { + Name: "gpu-h100", + Cpu: 64, + Memory: "432GB", + GpuModel: &h100, + GpuCount: &gpuCount, + }, + } + client := &FluidStackClient{ + apiKey: "test-api-key", + listInstanceTypes: func(context.Context) ([]openapi.InstanceType, error) { + return catalog, nil + }, + } + + unfiltered, err := client.GetInstanceTypes( + context.Background(), + cloudv1.GetInstanceTypeArgs{}, + ) + require.NoError(t, err) + require.Len(t, unfiltered, 2) + require.Equal(t, "gpu-gh200", unfiltered[0].Type) + require.Equal( + t, + []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + unfiltered[0].SupportedArchitectures, + ) + + filtered, err := client.GetInstanceTypes( + context.Background(), + cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{ + cloudv1.ArchitectureARM64, + }, + }, + }, + ) + require.NoError(t, err) + require.Len(t, filtered, 1) + require.Equal(t, "gpu-h100", filtered[0].Type) +} diff --git a/v1/providers/nebius/client.go b/v1/providers/nebius/client.go index 397b988..85aa088 100644 --- a/v1/providers/nebius/client.go +++ b/v1/providers/nebius/client.go @@ -26,6 +26,9 @@ type NebiusClient struct { location string sdk *gosdk.SDK logger v1.Logger + + listImages listNebiusImagesFunc + getInstanceTypePrice getInstanceTypePriceFunc } var _ v1.CloudClient = &NebiusClient{} diff --git a/v1/providers/nebius/image.go b/v1/providers/nebius/image.go index caf564a..eabfef3 100644 --- a/v1/providers/nebius/image.go +++ b/v1/providers/nebius/image.go @@ -9,7 +9,17 @@ import ( compute "github.com/nebius/gosdk/proto/nebius/compute/v1" ) +type listNebiusImagesFunc func(context.Context) []v1.Image + func (c *NebiusClient) GetImages(ctx context.Context, args v1.GetImageArgs) ([]v1.Image, error) { + listImages := c.listImages + if listImages == nil { + listImages = c.listImagesFromProvider + } + return applyImageFilters(listImages(ctx), args), nil +} + +func (c *NebiusClient) listImagesFromProvider(ctx context.Context) []v1.Image { var images []v1.Image // First, try to get project-specific images @@ -32,7 +42,7 @@ func (c *NebiusClient) GetImages(ctx context.Context, args v1.GetImageArgs) ([]v } } - return applyImageFilters(images, args), nil + return images } // getProjectImages retrieves images specific to the current project @@ -205,6 +215,9 @@ func extractArchitecture(image *compute.Image) string { return string(v1.ArchitectureX86_64) case compute.ImageSpec_ARM64: return string(v1.ArchitectureARM64) + case compute.ImageSpec_UNSPECIFIED: + default: + return string(v1.ArchitectureUnknown) } } diff --git a/v1/providers/nebius/image_test.go b/v1/providers/nebius/image_test.go index 5df26d3..c0bc873 100644 --- a/v1/providers/nebius/image_test.go +++ b/v1/providers/nebius/image_test.go @@ -1,6 +1,7 @@ package v1 import ( + "context" "testing" common "github.com/nebius/gosdk/proto/nebius/common/v1" @@ -48,6 +49,19 @@ func TestExtractArchitecture(t *testing.T) { }, want: string(cloudv1.ArchitectureARM64), }, + { + name: "unknown nonzero enum does not use legacy metadata", + image: &compute.Image{ + Metadata: &common.ResourceMetadata{ + Name: "ubuntu-24.04-arm64", + Labels: map[string]string{"architecture": "amd64"}, + }, + Spec: &compute.ImageSpec{ + CpuArchitecture: compute.ImageSpec_CPUArchitecture(99), + }, + }, + want: string(cloudv1.ArchitectureUnknown), + }, { name: "legacy label is canonicalized", image: &compute.Image{ @@ -94,3 +108,59 @@ func TestApplyImageFiltersWithoutArchitectureReturnsAll(t *testing.T) { applyImageFilters(images, cloudv1.GetImageArgs{}), ) } + +func TestNebiusClientGetImagesAppliesArchitectureFilter(t *testing.T) { + t.Parallel() + + client := &NebiusClient{ + listImages: func(context.Context) []cloudv1.Image { + return []cloudv1.Image{ + { + ID: "arm-image", + Name: "arm-image", + Architecture: string(cloudv1.ArchitectureARM64), + }, + { + ID: "x86-image", + Name: "x86-image", + Architecture: string(cloudv1.ArchitectureX86_64), + }, + } + }, + } + + unfiltered, err := client.GetImages(context.Background(), cloudv1.GetImageArgs{}) + require.NoError(t, err) + require.Equal( + t, + []cloudv1.Image{ + { + ID: "arm-image", + Name: "arm-image", + Architecture: string(cloudv1.ArchitectureARM64), + }, + { + ID: "x86-image", + Name: "x86-image", + Architecture: string(cloudv1.ArchitectureX86_64), + }, + }, + unfiltered, + ) + + filtered, err := client.GetImages(context.Background(), cloudv1.GetImageArgs{ + Architectures: []string{string(cloudv1.ArchitectureX86_64)}, + }) + require.NoError(t, err) + require.Equal( + t, + []cloudv1.Image{ + { + ID: "x86-image", + Name: "x86-image", + Architecture: string(cloudv1.ArchitectureX86_64), + }, + }, + filtered, + ) +} diff --git a/v1/providers/nebius/instance_test.go b/v1/providers/nebius/instance_test.go index 7b05dbc..4b10964 100644 --- a/v1/providers/nebius/instance_test.go +++ b/v1/providers/nebius/instance_test.go @@ -212,6 +212,14 @@ func TestGetGPUMemory(t *testing.T) { gpuType: "B200", expectedGiB: 192, }, + { + gpuType: "B300", + expectedGiB: 270, + }, + { + gpuType: "RTX6000", + expectedGiB: 96, + }, { gpuType: "UNKNOWN_GPU", expectedGiB: 0, @@ -277,6 +285,16 @@ func TestExtractGPUTypeAndName(t *testing.T) { expectedType: "B200", expectedName: "B200", }, + { + platformName: "gpu-b300-sxm", + expectedType: "B300", + expectedName: "B300", + }, + { + platformName: "gpu-rtx6000", + expectedType: "RTX6000", + expectedName: "RTX6000", + }, { platformName: "unknown-platform", expectedType: "GPU", @@ -338,29 +356,32 @@ func TestIsPlatformSupported(t *testing.T) { shouldSupport bool description string }{ - // GPU platforms - all should be supported + // Exact documented GPU platforms are supported. + {"gpu-b300-sxm", true, "B300 with gpu prefix"}, {"gpu-h100-sxm", true, "H100 with gpu prefix"}, {"gpu-h200-sxm", true, "H200 with gpu prefix"}, {"gpu-b200-sxm", true, "B200 with gpu prefix"}, - {"gpu-l40s", true, "L40S with gpu prefix"}, - {"gpu-a100-sxm4", true, "A100 with gpu prefix"}, - {"gpu-v100-sxm2", true, "V100 with gpu prefix"}, - {"gpu-a10", true, "A10 with gpu prefix"}, - {"gpu-t4", true, "T4 with gpu prefix"}, - {"gpu-l4", true, "L4 with gpu prefix"}, - - // GPU platforms without "gpu-" prefix (B200 specific test) - {"b200-sxm", true, "B200 without gpu prefix"}, - {"b200", true, "B200 bare name"}, - {"h100-sxm", true, "H100 without gpu prefix"}, - {"l40s", true, "L40S without gpu prefix"}, + {"gpu-b200-sxm-a", true, "B200 alternate platform"}, + {"gpu-rtx6000", true, "RTX PRO 6000 platform"}, + {"gpu-l40s-a", true, "L40S Intel platform"}, + {"gpu-l40s-d", true, "L40S AMD platform"}, // CPU platforms - only specific ones supported {"cpu-d3", true, "CPU D3 platform"}, {"cpu-e2", true, "CPU E2 platform"}, {"cpu-other", false, "Unsupported CPU platform"}, - // Unsupported platforms + // Legacy aliases and undocumented platforms are unsupported. + {"gpu-l40s", false, "L40S alias without documented suffix"}, + {"gpu-a100-sxm4", false, "A100 legacy platform"}, + {"gpu-v100-sxm2", false, "V100 legacy platform"}, + {"gpu-a10", false, "A10 legacy platform"}, + {"gpu-t4", false, "T4 legacy platform"}, + {"gpu-l4", false, "L4 legacy platform"}, + {"b200-sxm", false, "B200 without gpu prefix"}, + {"b200", false, "B200 bare name"}, + {"h100-sxm", false, "H100 without gpu prefix"}, + {"l40s", false, "L40S without gpu prefix"}, {"unknown-platform", false, "Generic unknown platform"}, {"random-gpu", false, "Random name with gpu"}, } diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index cf9ff7b..7f520eb 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -17,6 +17,13 @@ import ( quotas "github.com/nebius/gosdk/proto/nebius/quotas/v1" ) +type getInstanceTypePriceFunc func( + context.Context, + string, + string, + string, +) *currency.Amount + func (c *NebiusClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { // Get platforms (instance types) from Nebius API platformsResp, err := c.sdk.Services().Compute().V1().Platform().List(ctx, &compute.ListPlatformsRequest{ @@ -215,7 +222,12 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform } // Enrich with pricing information from Nebius Billing API - pricing := c.getPricingForInstanceType(ctx, platform.Metadata.Name, preset.Name, location.Name) + pricing := c.getPriceForInstanceType( + ctx, + platform.Metadata.Name, + preset.Name, + location.Name, + ) if pricing != nil { instanceType.BasePrice = pricing } @@ -463,23 +475,8 @@ func nebiusPlatformArchitecture(platformName string) v1.Architecture { // isPlatformSupported checks if a platform should be included in instance types func (c *NebiusClient) isPlatformSupported(platformName string) bool { - platformLower := strings.ToLower(platformName) - - // For GPU platforms: only accept known GPU types - // Check for specific GPU model names (with or without "gpu-" prefix) - knownGPUTypes := []string{"h100", "h200", "l40s", "a100", "v100", "a10", "t4", "l4", "b200"} - for _, gpuType := range knownGPUTypes { - if strings.Contains(platformLower, gpuType) { - return true - } - } - - // For CPU platforms: only accept specific types to avoid polluting the list - if strings.Contains(platformLower, "cpu-d3") || strings.Contains(platformLower, "cpu-e2") { - return true - } - - return false + _, ok := nebiusPlatformArchitectures[strings.ToLower(strings.TrimSpace(platformName))] + return ok } // isCPUOnlyPlatform checks if a platform is CPU-only (no GPUs) @@ -577,6 +574,12 @@ func extractGPUTypeAndName(platformName string) (string, string) { if strings.Contains(platformLower, "b200") { return "B200", "B200" } + if strings.Contains(platformLower, "b300") { + return "B300", "B300" + } + if strings.Contains(platformLower, "rtx6000") { + return "RTX6000", "RTX6000" + } return "GPU", "GPU" // Generic fallback } @@ -585,15 +588,17 @@ func extractGPUTypeAndName(platformName string) (string, string) { func getGPUMemory(gpuType string) units.Base2Bytes { // Static mapping of GPU types to their VRAM capacities vramMap := map[string]int64{ - "L40S": 48, // 48 GiB VRAM - "H100": 80, // 80 GiB VRAM - "H200": 141, // 141 GiB VRAM - "A100": 80, // 80 GiB VRAM (most common variant) - "V100": 32, // 32 GiB VRAM (most common variant) - "A10": 24, // 24 GiB VRAM - "T4": 16, // 16 GiB VRAM - "L4": 24, // 24 GiB VRAM - "B200": 192, // 192 GiB VRAM + "L40S": 48, // 48 GiB VRAM + "H100": 80, // 80 GiB VRAM + "H200": 141, // 141 GiB VRAM + "A100": 80, // 80 GiB VRAM (most common variant) + "V100": 32, // 32 GiB VRAM (most common variant) + "A10": 24, // 24 GiB VRAM + "T4": 16, // 16 GiB VRAM + "L4": 24, // 24 GiB VRAM + "B200": 192, // 192 GiB VRAM + "B300": 270, // 270 GiB VRAM + "RTX6000": 96, // 96 GiB VRAM } if vramGiB, exists := vramMap[gpuType]; exists { @@ -604,6 +609,18 @@ func getGPUMemory(gpuType string) units.Base2Bytes { return units.Base2Bytes(0) } +func (c *NebiusClient) getPriceForInstanceType( + ctx context.Context, + platformName string, + presetName string, + location string, +) *currency.Amount { + if c.getInstanceTypePrice != nil { + return c.getInstanceTypePrice(ctx, platformName, presetName, location) + } + return c.getPricingForInstanceType(ctx, platformName, presetName, location) +} + // getPricingForInstanceType fetches real pricing from Nebius Billing Calculator API // Returns nil if pricing cannot be fetched (non-critical failure) func (c *NebiusClient) getPricingForInstanceType(ctx context.Context, platformName, presetName, _ string) *currency.Amount { diff --git a/v1/providers/nebius/instancetype_test.go b/v1/providers/nebius/instancetype_test.go index 5500a43..ecf8032 100644 --- a/v1/providers/nebius/instancetype_test.go +++ b/v1/providers/nebius/instancetype_test.go @@ -1,8 +1,13 @@ package v1 import ( + "context" "testing" + "github.com/alecthomas/units" + "github.com/bojanz/currency" + common "github.com/nebius/gosdk/proto/nebius/common/v1" + compute "github.com/nebius/gosdk/proto/nebius/compute/v1" "github.com/stretchr/testify/require" cloudv1 "github.com/brevdev/cloud/v1" @@ -36,6 +41,109 @@ func TestNebiusPlatformArchitecture(t *testing.T) { } } +func TestGetInstanceTypesForLocationConstructsMappedPlatforms(t *testing.T) { + t.Parallel() + + client := &NebiusClient{ + projectID: "test-project", + logger: &cloudv1.NoopLogger{}, + getInstanceTypePrice: func( + context.Context, + string, + string, + string, + ) *currency.Amount { + return nil + }, + } + platforms := &compute.ListPlatformsResponse{ + Items: []*compute.Platform{ + { + Metadata: &common.ResourceMetadata{Name: "gpu-b300-sxm"}, + Spec: &compute.PlatformSpec{ + GpuMemoryGigabytes: 270, + Presets: []*compute.Preset{ + { + Name: "1gpu-24vcpu-346gb", + Resources: &compute.PresetResources{ + GpuCount: 1, + VcpuCount: 24, + MemoryGibibytes: 346, + }, + }, + }, + }, + }, + { + Metadata: &common.ResourceMetadata{Name: "gpu-rtx6000"}, + Spec: &compute.PlatformSpec{ + GpuMemoryGigabytes: 96, + Presets: []*compute.Preset{ + { + Name: "1gpu-24vcpu-218gb", + Resources: &compute.PresetResources{ + GpuCount: 1, + VcpuCount: 24, + MemoryGibibytes: 218, + }, + }, + }, + }, + }, + }, + } + + instanceTypes, err := client.getInstanceTypesForLocation( + context.Background(), + platforms, + cloudv1.Location{Name: "test-region"}, + cloudv1.GetInstanceTypeArgs{}, + nil, + nil, + ) + require.NoError(t, err) + require.Len(t, instanceTypes, 2) + + tests := []struct { + instanceType string + gpuType string + memoryGB int64 + }{ + { + instanceType: "gpu-b300-sxm.1gpu-24vcpu-346gb", + gpuType: "B300", + memoryGB: 270, + }, + { + instanceType: "gpu-rtx6000.1gpu-24vcpu-218gb", + gpuType: "RTX6000", + memoryGB: 96, + }, + } + for i, tt := range tests { + instanceType := instanceTypes[i] + require.Equal(t, tt.instanceType, instanceType.Type) + require.Equal( + t, + []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, + instanceType.SupportedArchitectures, + ) + require.False(t, instanceType.IsAvailable) + require.Len(t, instanceType.SupportedGPUs, 1) + + gpu := instanceType.SupportedGPUs[0] + require.Equal(t, int32(1), gpu.Count) + require.Equal(t, tt.gpuType, gpu.Type) + require.Equal(t, tt.gpuType, gpu.Name) + require.Equal(t, cloudv1.ManufacturerNVIDIA, gpu.Manufacturer) + require.Equal( + t, + units.Base2Bytes(tt.memoryGB*int64(units.Gibibyte)), + gpu.Memory, + ) + } +} + func TestApplyInstanceTypeFiltersUsesArchitectureMetadata(t *testing.T) { t.Parallel() From 5cd2e06bd89e783a7037c9f0327ea7a17d290a25 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Tue, 28 Jul 2026 08:46:56 -0700 Subject: [PATCH 12/17] revert: drop FluidStack ARM catalog changes --- .../2026-07-27-cloud-arm-instance-catalog.md | 196 +----------------- ...07-27-cloud-arm-instance-catalog-design.md | 10 +- v1/providers/fluidstack/client.go | 2 - v1/providers/fluidstack/instancetype.go | 70 ++----- v1/providers/fluidstack/instancetype_test.go | 136 ------------ 5 files changed, 23 insertions(+), 391 deletions(-) delete mode 100644 v1/providers/fluidstack/instancetype_test.go diff --git a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md index 38777ec..f97114d 100644 --- a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md +++ b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md @@ -36,7 +36,7 @@ | Launchpad | Authoritative `SystemArch` | Add mapping and filter characterization tests | | Shadeform | GPU-name inference | Use shared classifier | | LambdaLabs | Parsed GPU description, currently hardcoded x86 | Classify parsed Grace GPU models | -| FluidStack | `GpuModel`, currently no architecture metadata/filter | Populate metadata and honor only architecture filtering | +| FluidStack | Defunct; no active credential | No change | | Nebius instances | Platform IDs, name-based fallback to x86 | Explicit platform map, metadata-driven include/exclude filtering | | Nebius images | `ImageSpec.CpuArchitecture`, currently ignored | Prefer enum, remove unfiltered x86 default | | SFCompute v1/v2 | Static H100/H200 x86 metadata | Add regression coverage only | @@ -424,190 +424,10 @@ git commit -m "feat: report LambdaLabs Grace architectures" --- -### Task 5: Populate and filter FluidStack architecture metadata +### Task 5: Exclude FluidStack -**Files:** - -- Modify: `v1/providers/fluidstack/instancetype.go` -- Create: `v1/providers/fluidstack/instancetype_test.go` - -- [ ] **Step 1: Write failing converter and filter tests** - -Create `v1/providers/fluidstack/instancetype_test.go`: - -```go -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" - openapi "github.com/brevdev/cloud/v1/providers/fluidstack/gen/fluidstack" -) - -func TestConvertFluidStackInstanceTypeArchitecture(t *testing.T) { - t.Parallel() - - gh200 := "gh200" - b200 := "B200" - tests := []struct { - name string - gpuModel *string - want cloudv1.Architecture - }{ - {name: "GH200", gpuModel: &gh200, want: cloudv1.ArchitectureARM64}, - {name: "B200", gpuModel: &b200, want: cloudv1.ArchitectureX86_64}, - {name: "CPU", gpuModel: nil, want: cloudv1.ArchitectureX86_64}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - gpuCount := int32(1) - if tt.gpuModel == nil { - gpuCount = 0 - } - got := convertFluidStackInstanceTypeToV1InstanceType( - "", - openapi.InstanceType{ - Name: "test-" + tt.name, - Cpu: 64, - Memory: "432GB", - GpuModel: tt.gpuModel, - GpuCount: &gpuCount, - }, - true, - ) - require.Equal(t, []cloudv1.Architecture{tt.want}, got.SupportedArchitectures) - }) - } -} - -func TestFilterFluidStackInstanceTypesByArchitecture(t *testing.T) { - t.Parallel() - - x86Type := cloudv1.InstanceType{ - Type: "x86", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - } - armType := cloudv1.InstanceType{ - Type: "arm", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, - } - instanceTypes := []cloudv1.InstanceType{x86Type, armType} - - require.Equal( - t, - instanceTypes, - filterInstanceTypesByArchitecture(instanceTypes, nil), - ) - require.Equal( - t, - []cloudv1.InstanceType{x86Type}, - filterInstanceTypesByArchitecture( - instanceTypes, - &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, - }, - ), - ) -} -``` - -- [ ] **Step 2: Run the focused tests and confirm the red state** - -Run: - -```bash -go test ./v1/providers/fluidstack -run 'Architecture' -count=1 -``` - -Expected: the filter helper is undefined and the converter emits no -`SupportedArchitectures`. - -- [ ] **Step 3: Populate architecture metadata during conversion** - -Before the returned `v1.InstanceType` literal, add: - -```go -architecture := v1.ArchitectureX86_64 -if fsInstanceType.GpuModel != nil && - v1.IsNVIDIAGraceGPU(*fsInstanceType.GpuModel) { - architecture = v1.ArchitectureARM64 -} -``` - -Add this field to the returned literal: - -```go -SupportedArchitectures: []v1.Architecture{architecture}, -``` - -- [ ] **Step 4: Add architecture-only filtering** - -Change the `GetInstanceTypes` signature to retain `args`: - -```go -func (c *FluidStackClient) GetInstanceTypes( - ctx context.Context, - args v1.GetInstanceTypeArgs, -) ([]v1.InstanceType, error) { -``` - -Return the architecture-filtered values: - -```go -return filterInstanceTypesByArchitecture( - instanceTypes, - args.ArchitectureFilter, -), nil -``` - -Add this provider-local helper: - -```go -func filterInstanceTypesByArchitecture( - instanceTypes []v1.InstanceType, - filter *v1.ArchitectureFilter, -) []v1.InstanceType { - if filter == nil { - return instanceTypes - } - - filtered := make([]v1.InstanceType, 0, len(instanceTypes)) - for _, instanceType := range instanceTypes { - for _, architecture := range instanceType.SupportedArchitectures { - if filter.IsAllowed(architecture) { - filtered = append(filtered, instanceType) - break - } - } - } - return filtered -} -``` - -Do not add location or instance-name filtering in this task. - -- [ ] **Step 5: Format and run the complete FluidStack package** - -Run: - -```bash -gofmt -w v1/providers/fluidstack/instancetype.go v1/providers/fluidstack/instancetype_test.go -go test ./v1/providers/fluidstack -count=1 -``` - -Expected: all FluidStack tests pass. - -- [ ] **Step 6: Commit the provider change** - -```bash -git add v1/providers/fluidstack/instancetype.go v1/providers/fluidstack/instancetype_test.go -git commit -m "feat: expose FluidStack instance architectures" -``` +FluidStack is defunct and no active credential remains. Make no FluidStack +provider changes as part of this work. --- @@ -1172,8 +992,6 @@ gofmt -w \ v1/providers/shadeform/instancetype_test.go \ v1/providers/lambdalabs/instancetype.go \ v1/providers/lambdalabs/instancetype_test.go \ - v1/providers/fluidstack/instancetype.go \ - v1/providers/fluidstack/instancetype_test.go \ v1/providers/nebius/instancetype.go \ v1/providers/nebius/instancetype_test.go \ v1/providers/nebius/image.go \ @@ -1192,7 +1010,6 @@ go test \ ./v1/providers/launchpad \ ./v1/providers/shadeform \ ./v1/providers/lambdalabs \ - ./v1/providers/fluidstack \ ./v1/providers/nebius \ ./v1/providers/sfcompute \ ./v1/providers/sfcomputev2 \ @@ -1238,7 +1055,6 @@ git diff HEAD~8..HEAD -- \ v1/providers/launchpad \ v1/providers/shadeform \ v1/providers/lambdalabs \ - v1/providers/fluidstack \ v1/providers/nebius \ v1/providers/sfcompute \ v1/providers/sfcomputev2 @@ -1249,8 +1065,8 @@ Confirm: - no dev-plane or Verb files changed; - no generated clients changed; - Launchpad still uses `SystemArch`; -- Shadeform, LambdaLabs, and FluidStack share only Grace recognition; -- FluidStack added only architecture filtering; +- Shadeform and LambdaLabs share only Grace recognition; +- FluidStack remains unchanged; - Nebius unknown platforms and images remain `unknown`; - no synthetic Nebius or TestKube ARM offering was introduced; - ARM provisioning and boot-image selection remain unchanged. diff --git a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md index a2a5185..76a277d 100644 --- a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md +++ b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md @@ -80,13 +80,8 @@ GH200 type when ARM is excluded. ### FluidStack -Populate `SupportedArchitectures` from `GpuModel` using the same Grace rule. -Known non-Grace and CPU offerings remain x86_64. - -FluidStack currently ignores `GetInstanceTypeArgs`. Add architecture-filter -handling without broadening this change into location or instance-name -filtering. This is required so dev-plane's existing ARM exclusion remains -effective. +FluidStack is defunct and no active credential remains. Leave its provider +adapter unchanged and exclude it from this work. ### Nebius instance types @@ -181,7 +176,6 @@ Focused tests will cover: - Launchpad authoritative AMD64 and ARM64 conversion; - Shadeform GH/GB conversion; - LambdaLabs GH200 conversion and architecture filtering; -- FluidStack conversion and architecture filtering; - Nebius known x86 and unknown platform mappings; - Nebius include and exclude filter behavior; - Nebius authoritative AMD64/ARM64 image metadata and unfiltered image listing; diff --git a/v1/providers/fluidstack/client.go b/v1/providers/fluidstack/client.go index 4b5b542..5016d52 100644 --- a/v1/providers/fluidstack/client.go +++ b/v1/providers/fluidstack/client.go @@ -70,8 +70,6 @@ type FluidStackClient struct { baseURL string projectID string client *openapi.APIClient - - listInstanceTypes func(context.Context) ([]openapi.InstanceType, error) } var _ v1.CloudClient = &FluidStackClient{} diff --git a/v1/providers/fluidstack/instancetype.go b/v1/providers/fluidstack/instancetype.go index 0393fab..a677966 100644 --- a/v1/providers/fluidstack/instancetype.go +++ b/v1/providers/fluidstack/instancetype.go @@ -13,15 +13,14 @@ import ( openapi "github.com/brevdev/cloud/v1/providers/fluidstack/gen/fluidstack" ) -func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { +func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, _ v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { authCtx := c.makeAuthContext(ctx) projectCtx := c.makeProjectContext(authCtx) - listInstanceTypes := c.listInstanceTypes - if listInstanceTypes == nil { - listInstanceTypes = c.listInstanceTypesFromAPI + resp, httpResp, err := c.client.InstanceTypesAPI.ListInstanceTypes(projectCtx).Execute() + if httpResp != nil && httpResp.Body != nil { + defer func() { _ = httpResp.Body.Close() }() } - resp, err := listInstanceTypes(projectCtx) if err != nil { return nil, fmt.Errorf("failed to get instance types: %w", err) } @@ -32,39 +31,7 @@ func (c *FluidStackClient) GetInstanceTypes(ctx context.Context, args v1.GetInst instanceTypes = append(instanceTypes, instanceType) } - return filterInstanceTypesByArchitecture(instanceTypes, args.ArchitectureFilter), nil -} - -func (c *FluidStackClient) listInstanceTypesFromAPI(ctx context.Context) ([]openapi.InstanceType, error) { - resp, httpResp, err := c.client.InstanceTypesAPI.ListInstanceTypes(ctx).Execute() - if httpResp != nil && httpResp.Body != nil { - defer func() { _ = httpResp.Body.Close() }() - } - if err != nil { - return nil, err - } - - return resp, nil -} - -func filterInstanceTypesByArchitecture( - instanceTypes []v1.InstanceType, - filter *v1.ArchitectureFilter, -) []v1.InstanceType { - if filter == nil { - return instanceTypes - } - - filtered := make([]v1.InstanceType, 0, len(instanceTypes)) - for _, instanceType := range instanceTypes { - for _, architecture := range instanceType.SupportedArchitectures { - if filter.IsAllowed(architecture) { - filtered = append(filtered, instanceType) - break - } - } - } - return filtered + return instanceTypes, nil } func (c *FluidStackClient) GetInstanceTypePollTime() time.Duration { @@ -133,23 +100,16 @@ func convertFluidStackInstanceTypeToV1InstanceType(location string, fsInstanceTy price, _ := currency.NewAmount("0", "USD") - architecture := v1.ArchitectureX86_64 - if fsInstanceType.GpuModel != nil && - v1.IsNVIDIAGraceGPU(*fsInstanceType.GpuModel) { - architecture = v1.ArchitectureARM64 - } - return v1.InstanceType{ - Type: fsInstanceType.Name, - VCPU: vcpus, - Memory: ram, - MemoryBytes: memoryBytes, - SupportedGPUs: gpus, - SupportedArchitectures: []v1.Architecture{architecture}, - BasePrice: &price, - IsAvailable: isAvailable, - Location: location, - Provider: CloudProviderID, - Cloud: CloudProviderID, + Type: fsInstanceType.Name, + VCPU: vcpus, + Memory: ram, + MemoryBytes: memoryBytes, + SupportedGPUs: gpus, + BasePrice: &price, + IsAvailable: isAvailable, + Location: location, + Provider: CloudProviderID, + Cloud: CloudProviderID, } } diff --git a/v1/providers/fluidstack/instancetype_test.go b/v1/providers/fluidstack/instancetype_test.go deleted file mode 100644 index 1fc64ac..0000000 --- a/v1/providers/fluidstack/instancetype_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package v1 - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" - openapi "github.com/brevdev/cloud/v1/providers/fluidstack/gen/fluidstack" -) - -func TestConvertFluidStackInstanceTypeArchitecture(t *testing.T) { - t.Parallel() - - gh200 := "gh200" - b200 := "B200" - tests := []struct { - name string - gpuModel *string - want cloudv1.Architecture - }{ - {name: "GH200", gpuModel: &gh200, want: cloudv1.ArchitectureARM64}, - {name: "B200", gpuModel: &b200, want: cloudv1.ArchitectureX86_64}, - {name: "CPU", gpuModel: nil, want: cloudv1.ArchitectureX86_64}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - gpuCount := int32(1) - if tt.gpuModel == nil { - gpuCount = 0 - } - got := convertFluidStackInstanceTypeToV1InstanceType( - "", - openapi.InstanceType{ - Name: "test-" + tt.name, - Cpu: 64, - Memory: "432GB", - GpuModel: tt.gpuModel, - GpuCount: &gpuCount, - }, - true, - ) - require.Equal(t, []cloudv1.Architecture{tt.want}, got.SupportedArchitectures) - }) - } -} - -func TestFilterFluidStackInstanceTypesByArchitecture(t *testing.T) { - t.Parallel() - - x86Type := cloudv1.InstanceType{ - Type: "x86", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - } - armType := cloudv1.InstanceType{ - Type: "arm", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, - } - instanceTypes := []cloudv1.InstanceType{x86Type, armType} - - require.Equal( - t, - instanceTypes, - filterInstanceTypesByArchitecture(instanceTypes, nil), - ) - require.Equal( - t, - []cloudv1.InstanceType{x86Type}, - filterInstanceTypesByArchitecture( - instanceTypes, - &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, - }, - ), - ) -} - -func TestFluidStackClientGetInstanceTypesAppliesArchitectureFilter(t *testing.T) { - t.Parallel() - - gh200 := "GH200" - h100 := "H100" - gpuCount := int32(1) - catalog := []openapi.InstanceType{ - { - Name: "gpu-gh200", - Cpu: 64, - Memory: "432GB", - GpuModel: &gh200, - GpuCount: &gpuCount, - }, - { - Name: "gpu-h100", - Cpu: 64, - Memory: "432GB", - GpuModel: &h100, - GpuCount: &gpuCount, - }, - } - client := &FluidStackClient{ - apiKey: "test-api-key", - listInstanceTypes: func(context.Context) ([]openapi.InstanceType, error) { - return catalog, nil - }, - } - - unfiltered, err := client.GetInstanceTypes( - context.Background(), - cloudv1.GetInstanceTypeArgs{}, - ) - require.NoError(t, err) - require.Len(t, unfiltered, 2) - require.Equal(t, "gpu-gh200", unfiltered[0].Type) - require.Equal( - t, - []cloudv1.Architecture{cloudv1.ArchitectureARM64}, - unfiltered[0].SupportedArchitectures, - ) - - filtered, err := client.GetInstanceTypes( - context.Background(), - cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - ) - require.NoError(t, err) - require.Len(t, filtered, 1) - require.Equal(t, "gpu-h100", filtered[0].Type) -} From 01c3d1eb8ca99e352740511274abcd11627b195b Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Tue, 28 Jul 2026 09:07:18 -0700 Subject: [PATCH 13/17] revert: drop deprecated SFCompute ARM coverage --- .../2026-07-27-cloud-arm-instance-catalog.md | 59 +++++-------------- ...07-27-cloud-arm-instance-catalog-design.md | 17 ++++-- v1/providers/sfcompute/instancetype_test.go | 22 ------- 3 files changed, 25 insertions(+), 73 deletions(-) delete mode 100644 v1/providers/sfcompute/instancetype_test.go diff --git a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md index f97114d..d933f4b 100644 --- a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md +++ b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md @@ -39,7 +39,8 @@ | FluidStack | Defunct; no active credential | No change | | Nebius instances | Platform IDs, name-based fallback to x86 | Explicit platform map, metadata-driven include/exclude filtering | | Nebius images | `ImageSpec.CpuArchitecture`, currently ignored | Prefer enum, remove unfiltered x86 default | -| SFCompute v1/v2 | Static H100/H200 x86 metadata | Add regression coverage only | +| SFCompute v1 | Deprecated in favor of v2 | No change | +| SFCompute v2 | Static H100 x86 metadata | Add regression coverage only | --- @@ -874,45 +875,14 @@ git commit -m "feat: expose Nebius image architectures" --- -### Task 8: Record SFCompute's current x86-only provider contract +### Task 8: Record SFComputeV2's current x86-only provider contract **Files:** -- Create: `v1/providers/sfcompute/instancetype_test.go` - Create: `v1/providers/sfcomputev2/instancetype_test.go` -- Verify only: `v1/providers/sfcompute/instancetype.go` - Verify only: `v1/providers/sfcomputev2/instancetype.go` -- [ ] **Step 1: Add SFCompute v1 characterization coverage** - -Create `v1/providers/sfcompute/instancetype_test.go`: - -```go -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" -) - -func TestSFComputeInstanceTypeArchitectures(t *testing.T) { - t.Parallel() - - for _, gpuType := range []string{gpuTypeH100, gpuTypeH200} { - t.Run(gpuType, func(t *testing.T) { - t.Parallel() - metadata, err := getInstanceTypeMetadata(gpuType) - require.NoError(t, err) - require.Equal(t, cloudv1.ArchitectureX86_64, metadata.architecture) - }) - } -} -``` - -- [ ] **Step 2: Add SFCompute v2 characterization and filter coverage** +- [ ] **Step 1: Add SFComputeV2 characterization and filter coverage** Create `v1/providers/sfcomputev2/instancetype_test.go`: @@ -952,23 +922,23 @@ func TestSFComputeV2InstanceTypeArchitecture(t *testing.T) { } ``` -- [ ] **Step 3: Run both characterization packages** +- [ ] **Step 2: Run the characterization package** Run: ```bash -gofmt -w v1/providers/sfcompute/instancetype_test.go v1/providers/sfcomputev2/instancetype_test.go -go test ./v1/providers/sfcompute ./v1/providers/sfcomputev2 -count=1 +gofmt -w v1/providers/sfcomputev2/instancetype_test.go +go test ./v1/providers/sfcomputev2 -count=1 ``` -Expected: tests pass without production changes. H100 and H200 remain truthfully -x86 until SFCompute exposes an authoritative Grace SKU and compatible image. +Expected: tests pass without production changes. H100 remains truthfully x86 +until SFComputeV2 exposes an authoritative Grace SKU and compatible image. -- [ ] **Step 4: Commit the contract tests** +- [ ] **Step 3: Commit the contract test** ```bash -git add v1/providers/sfcompute/instancetype_test.go v1/providers/sfcomputev2/instancetype_test.go -git commit -m "test: record SFCompute architecture contracts" +git add v1/providers/sfcomputev2/instancetype_test.go +git commit -m "test: record SFComputeV2 architecture contract" ``` --- @@ -996,7 +966,6 @@ gofmt -w \ v1/providers/nebius/instancetype_test.go \ v1/providers/nebius/image.go \ v1/providers/nebius/image_test.go \ - v1/providers/sfcompute/instancetype_test.go \ v1/providers/sfcomputev2/instancetype_test.go ``` @@ -1011,7 +980,6 @@ go test \ ./v1/providers/shadeform \ ./v1/providers/lambdalabs \ ./v1/providers/nebius \ - ./v1/providers/sfcompute \ ./v1/providers/sfcomputev2 \ -count=1 ``` @@ -1056,7 +1024,6 @@ git diff HEAD~8..HEAD -- \ v1/providers/shadeform \ v1/providers/lambdalabs \ v1/providers/nebius \ - v1/providers/sfcompute \ v1/providers/sfcomputev2 ``` @@ -1067,6 +1034,8 @@ Confirm: - Launchpad still uses `SystemArch`; - Shadeform and LambdaLabs share only Grace recognition; - FluidStack remains unchanged; +- SFCompute remains unchanged; +- SFComputeV2 still reports its existing x86-only contract; - Nebius unknown platforms and images remain `unknown`; - no synthetic Nebius or TestKube ARM offering was introduced; - ARM provisioning and boot-image selection remain unchanged. diff --git a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md index 76a277d..f0b984a 100644 --- a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md +++ b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md @@ -113,16 +113,21 @@ Remove the catalog-level x86 restriction: This changes image discovery only. Architecture-aware boot-image selection is part of the later provisioning work. -### SFCompute and SFComputeV2 +### SFCompute -Make no production classification change. The current providers expose H100 -and H200 offerings backed by x86-only image requirements, and their metadata -already reports `x86_64`. +SFCompute is deprecated in favor of SFComputeV2. Leave the v1 provider +unchanged and exclude it from this work. + +### SFComputeV2 + +Make no production classification change. The current provider exposes an H100 +offering backed by x86-only image requirements, and its metadata already +reports `x86_64`. Add focused regression coverage where practical so this is recorded as a provider capability rather than an accidental ARM restriction. -Future SFCompute ARM support requires an authoritative Grace SKU plus +Future SFComputeV2 ARM support requires an authoritative Grace SKU plus architecture-compatible image and create-path support; it is not part of this catalog change. @@ -179,7 +184,7 @@ Focused tests will cover: - Nebius known x86 and unknown platform mappings; - Nebius include and exclude filter behavior; - Nebius authoritative AMD64/ARM64 image metadata and unfiltered image listing; -- existing SFCompute x86 metadata where a focused unit seam exists. +- existing SFComputeV2 x86 metadata where a focused unit seam exists. Run package-level tests while iterating, then: diff --git a/v1/providers/sfcompute/instancetype_test.go b/v1/providers/sfcompute/instancetype_test.go deleted file mode 100644 index fc83dd2..0000000 --- a/v1/providers/sfcompute/instancetype_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" -) - -func TestSFComputeInstanceTypeArchitectures(t *testing.T) { - t.Parallel() - - for _, gpuType := range []string{gpuTypeH100, gpuTypeH200} { - t.Run(gpuType, func(t *testing.T) { - t.Parallel() - metadata, err := getInstanceTypeMetadata(gpuType) - require.NoError(t, err) - require.Equal(t, cloudv1.ArchitectureX86_64, metadata.architecture) - }) - } -} From e3410b87b3e8af7f159240718de1ac4678ab6d28 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Tue, 28 Jul 2026 12:54:29 -0700 Subject: [PATCH 14/17] chore: slim ARM catalog change --- .../2026-07-27-cloud-arm-instance-catalog.md | 1052 ----------------- ...07-27-cloud-arm-instance-catalog-design.md | 207 ---- v1/instancetype.go | 7 - v1/instancetype_test.go | 28 - v1/providers/launchpad/instancetype_test.go | 39 - v1/providers/nebius/client.go | 3 - v1/providers/nebius/image.go | 12 +- v1/providers/nebius/image_test.go | 161 +-- v1/providers/nebius/instance_test.go | 33 +- v1/providers/nebius/instancetype.go | 47 +- v1/providers/nebius/instancetype_test.go | 225 +--- v1/providers/sfcomputev2/instancetype_test.go | 33 - v1/providers/shadeform/instancetype.go | 3 +- v1/providers/shadeform/instancetype_test.go | 23 - 14 files changed, 93 insertions(+), 1780 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md delete mode 100644 docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md delete mode 100644 v1/providers/sfcomputev2/instancetype_test.go diff --git a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md b/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md deleted file mode 100644 index d933f4b..0000000 --- a/docs/superpowers/plans/2026-07-27-cloud-arm-instance-catalog.md +++ /dev/null @@ -1,1052 +0,0 @@ -# Cloud ARM Instance Catalog Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make Cloud return ARM-capable instance and image catalog entries with truthful canonical architecture metadata while preserving dev-plane's existing ARM exclusion. - -**Architecture:** Keep architecture discovery inside each provider adapter, normalize shared NVIDIA Grace model detection in `v1`, and apply the existing caller-owned architecture filter after metadata is populated. Providers with authoritative architecture fields retain those fields as the source of truth; inferred or mapped providers return `unknown` when the architecture cannot be established. - -**Tech Stack:** Go 1.25, generated provider SDK clients, `testify`, package-level Go tests, `gofmt`, and `golangci-lint`. - -## Global Constraints - -- The approved design is - [`docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md`](../specs/2026-07-27-cloud-arm-instance-catalog-design.md). -- Do not modify dev-plane. Its existing `ArchitectureFilter` continues to exclude - ARM from the synchronized catalog. -- Do not change or remove the public `ArchitectureFilter` contract. -- Do not enable ARM provisioning, image selection, TestKube ARM scheduling, or - Verb mode. -- An unfiltered Cloud call returns all known entries. A filtered call honors the - caller's filter. -- Emit only canonical values: `x86_64`, `arm64`, or `unknown`. -- Do not classify B200 or B300 as ARM merely because they are Blackwell GPUs. -- Nebius's current documented platform IDs are mapped explicitly. The current - platform table documents Intel or AMD host CPUs for all of them: - . -- Follow red-green-refactor for every behavior change. Characterization tests - for already-correct providers are expected to pass immediately. -- Run `gofmt` on every touched Go file. Do not edit generated provider clients. - -## File and Interface Map - -| Area | Existing contract | Planned touchpoint | -| --- | --- | --- | -| Shared types | `v1.Architecture`, `ArchitectureFilter.IsAllowed` | Add shared Grace model classifier | -| Launchpad | Authoritative `SystemArch` | Add mapping and filter characterization tests | -| Shadeform | GPU-name inference | Use shared classifier | -| LambdaLabs | Parsed GPU description, currently hardcoded x86 | Classify parsed Grace GPU models | -| FluidStack | Defunct; no active credential | No change | -| Nebius instances | Platform IDs, name-based fallback to x86 | Explicit platform map, metadata-driven include/exclude filtering | -| Nebius images | `ImageSpec.CpuArchitecture`, currently ignored | Prefer enum, remove unfiltered x86 default | -| SFCompute v1 | Deprecated in favor of v2 | No change | -| SFCompute v2 | Static H100 x86 metadata | Add regression coverage only | - ---- - -### Task 1: Add the shared NVIDIA Grace classifier - -**Files:** - -- Modify: `v1/instancetype.go` -- Modify: `v1/instancetype_test.go` - -- [ ] **Step 1: Write the failing classifier test** - -Append this table test to `v1/instancetype_test.go`: - -```go -func TestIsNVIDIAGraceGPU(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - gpuModel string - want bool - }{ - {name: "GH200", gpuModel: "GH200", want: true}, - {name: "GB200", gpuModel: "GB200", want: true}, - {name: "lowercase GH200", gpuModel: "gh200", want: true}, - {name: "trimmed GB200", gpuModel: " gb200 ", want: true}, - {name: "vendor-prefixed GH200", gpuModel: "NVIDIA GH200", want: true}, - {name: "H100", gpuModel: "H100", want: false}, - {name: "B200", gpuModel: "B200", want: false}, - {name: "empty", gpuModel: "", want: false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if got := IsNVIDIAGraceGPU(tt.gpuModel); got != tt.want { - t.Fatalf("IsNVIDIAGraceGPU(%q) = %v, want %v", tt.gpuModel, got, tt.want) - } - }) - } -} -``` - -- [ ] **Step 2: Run the focused test and confirm the red state** - -Run: - -```bash -go test ./v1 -run '^TestIsNVIDIAGraceGPU$' -count=1 -``` - -Expected: compilation fails because `IsNVIDIAGraceGPU` is undefined. - -- [ ] **Step 3: Implement the smallest shared classifier** - -Add this function beside the existing architecture normalization helpers in -`v1/instancetype.go`: - -```go -func IsNVIDIAGraceGPU(gpuModel string) bool { - normalizedModel := strings.ToUpper(strings.TrimSpace(gpuModel)) - normalizedModel = strings.TrimPrefix(normalizedModel, "NVIDIA ") - return strings.HasPrefix(normalizedModel, "GH") || - strings.HasPrefix(normalizedModel, "GB") -} -``` - -The function deliberately answers only whether the model is a Grace product. -It does not choose a fallback architecture. - -- [ ] **Step 4: Format and prove the green state** - -Run: - -```bash -gofmt -w v1/instancetype.go v1/instancetype_test.go -go test ./v1 -run '^(TestGetArchitectureAliases|TestIsNVIDIAGraceGPU)$' -count=1 -``` - -Expected: both tests pass. - -- [ ] **Step 5: Commit the shared behavior** - -```bash -git add v1/instancetype.go v1/instancetype_test.go -git commit -m "feat: classify NVIDIA Grace GPU models" -``` - ---- - -### Task 2: Lock Launchpad's authoritative architecture behavior - -**Files:** - -- Modify: `v1/providers/launchpad/instancetype_test.go` -- Verify only: `v1/providers/launchpad/instancetype.go` - -- [ ] **Step 1: Add mapping and caller-filter characterization tests** - -Add the generated Launchpad package import: - -```go -openapi "github.com/brevdev/cloud/v1/providers/launchpad/gen/launchpad" -``` - -Then append: - -```go -func TestLaunchpadArchitectureToArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - raw openapi.SystemArchEnum - want v1.Architecture - }{ - {name: "AMD64", raw: openapi.SystemArchAMD64, want: v1.ArchitectureX86_64}, - {name: "ARM64", raw: openapi.SystemArchARM64, want: v1.ArchitectureARM64}, - {name: "unknown", raw: openapi.SystemArchEnum("riscv64"), want: v1.ArchitectureUnknown}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, launchpadArchitectureToArchitecture(tt.raw)) - }) - } -} - -func TestLaunchpadArchitectureFilterExcludesARM(t *testing.T) { - t.Parallel() - - instanceType := v1.InstanceType{ - SupportedArchitectures: []v1.Architecture{ - launchpadArchitectureToArchitecture(openapi.SystemArchARM64), - }, - } - args := v1.GetInstanceTypeArgs{ - ArchitectureFilter: &v1.ArchitectureFilter{ - ExcludeArchitectures: []v1.Architecture{v1.ArchitectureARM64}, - }, - } - - require.False(t, v1.IsSelectedByArgs(instanceType, args)) -} -``` - -- [ ] **Step 2: Run the characterization tests** - -Run: - -```bash -go test ./v1/providers/launchpad -run '^TestLaunchpadArchitecture' -count=1 -``` - -Expected: tests pass without production changes because Launchpad already uses -its authoritative `SystemArch` value and the common filter. - -- [ ] **Step 3: Commit the contract tests** - -```bash -git add v1/providers/launchpad/instancetype_test.go -git commit -m "test: lock Launchpad architecture mapping" -``` - ---- - -### Task 3: Move Shadeform Grace inference to the shared classifier - -**Files:** - -- Modify: `v1/providers/shadeform/instancetype.go` -- Modify: `v1/providers/shadeform/instancetype_test.go` - -- [ ] **Step 1: Write the failing case-normalization test** - -Append: - -```go -func TestShadeformArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - gpuModel string - want v1.Architecture - }{ - {gpuModel: "GH200", want: v1.ArchitectureARM64}, - {gpuModel: "GB200", want: v1.ArchitectureARM64}, - {gpuModel: "gh200", want: v1.ArchitectureARM64}, - {gpuModel: "gb200", want: v1.ArchitectureARM64}, - {gpuModel: "H100", want: v1.ArchitectureX86_64}, - {gpuModel: "B200", want: v1.ArchitectureX86_64}, - } - - for _, tt := range tests { - t.Run(tt.gpuModel, func(t *testing.T) { - t.Parallel() - assert.Equal(t, tt.want, shadeformArchitecture(tt.gpuModel)) - }) - } -} -``` - -- [ ] **Step 2: Run the focused test and confirm the red state** - -Run: - -```bash -go test ./v1/providers/shadeform -run '^TestShadeformArchitecture$' -count=1 -``` - -Expected: lowercase GH200 and GB200 cases fail because the current check is -case-sensitive. - -- [ ] **Step 3: Delegate classification to the shared helper** - -Replace `shadeformArchitecture` with: - -```go -func shadeformArchitecture(gpuName string) v1.Architecture { - if v1.IsNVIDIAGraceGPU(gpuName) { - return v1.ArchitectureARM64 - } - return v1.ArchitectureX86_64 -} -``` - -Keep the provider-owned x86 fallback; only recognition is shared. - -- [ ] **Step 4: Format and run the package tests** - -Run: - -```bash -gofmt -w v1/providers/shadeform/instancetype.go v1/providers/shadeform/instancetype_test.go -go test ./v1/providers/shadeform -count=1 -``` - -Expected: all Shadeform tests pass. - -- [ ] **Step 5: Commit the provider change** - -```bash -git add v1/providers/shadeform/instancetype.go v1/providers/shadeform/instancetype_test.go -git commit -m "feat: normalize Shadeform Grace architectures" -``` - ---- - -### Task 4: Classify LambdaLabs Grace offerings and preserve filtering - -**Files:** - -- Modify: `v1/providers/lambdalabs/instancetype.go` -- Modify: `v1/providers/lambdalabs/instancetype_test.go` - -- [ ] **Step 1: Write failing conversion and end-to-end filter tests** - -Append these tests: - -```go -func TestConvertLambdaLabsInstanceTypeArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - description string - want v1.Architecture - }{ - {name: "GH200", description: "1x GH200 (96 GB)", want: v1.ArchitectureARM64}, - {name: "NVIDIA GH200", description: "1x NVIDIA GH200 (96 GB)", want: v1.ArchitectureARM64}, - {name: "H100", description: "1x H100 (80 GB SXM5)", want: v1.ArchitectureX86_64}, - {name: "B200", description: "1x B200 (192 GB SXM6)", want: v1.ArchitectureX86_64}, - {name: "CPU", description: "4x CPU cores", want: v1.ArchitectureX86_64}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - instanceType := createMockLambdaLabsInstanceType( - "test-"+tt.name, - tt.description, - tt.name, - 100, - ) - - got, err := convertLambdaLabsInstanceTypeToV1InstanceType( - "us-west-1", - instanceType, - true, - ) - require.NoError(t, err) - require.Equal(t, []v1.Architecture{tt.want}, got.SupportedArchitectures) - }) - } -} - -func TestLambdaLabsClient_GetInstanceTypes_ExcludeARM(t *testing.T) { - client, cleanup := setupMockClient() - defer cleanup() - - mockResponse := createMockInstanceTypeResponse() - mockResponse.Data["gpu_1x_gh200"] = openapi.InstanceTypes200ResponseDataValue{ - InstanceType: createMockLambdaLabsInstanceType( - "gpu_1x_gh200", - "1x GH200 (96 GB)", - "GH200", - 229, - ), - RegionsWithCapacityAvailable: []openapi.Region{ - createMockRegion("us-west-1", "US West 1"), - }, - } - httpmock.RegisterResponder( - "GET", - "https://cloud.lambda.ai/api/v1/instance-types", - httpmock.NewJsonResponderOrPanic(200, mockResponse), - ) - - instanceTypes, err := client.GetInstanceTypes( - context.Background(), - v1.GetInstanceTypeArgs{ - ArchitectureFilter: &v1.ArchitectureFilter{ - ExcludeArchitectures: []v1.Architecture{v1.ArchitectureARM64}, - }, - }, - ) - require.NoError(t, err) - require.Nil(t, findInstanceTypeByName(instanceTypes, "gpu_1x_gh200")) - require.NotNil(t, findInstanceTypeByName(instanceTypes, "gpu_1x_a10")) -} -``` - -- [ ] **Step 2: Run the focused tests and confirm the red state** - -Run: - -```bash -go test ./v1/providers/lambdalabs -run 'Architecture|ExcludeARM' -count=1 -``` - -Expected: GH200 is reported as x86 and survives the ARM exclusion. - -- [ ] **Step 3: Derive architecture from the parsed GPU model** - -After parsing `gpus` and before constructing the returned instance type, add: - -```go -architecture := v1.ArchitectureX86_64 -if len(gpus) > 0 && v1.IsNVIDIAGraceGPU(gpus[0].Name) { - architecture = v1.ArchitectureARM64 -} -``` - -Then replace the hardcoded field with: - -```go -SupportedArchitectures: []v1.Architecture{architecture}, -``` - -CPU-only and non-Grace offerings deliberately retain the provider-owned x86 -fallback. - -- [ ] **Step 4: Format and run the complete LambdaLabs package** - -Run: - -```bash -gofmt -w v1/providers/lambdalabs/instancetype.go v1/providers/lambdalabs/instancetype_test.go -go test ./v1/providers/lambdalabs -count=1 -``` - -Expected: all LambdaLabs tests pass. - -- [ ] **Step 5: Commit the provider change** - -```bash -git add v1/providers/lambdalabs/instancetype.go v1/providers/lambdalabs/instancetype_test.go -git commit -m "feat: report LambdaLabs Grace architectures" -``` - ---- - -### Task 5: Exclude FluidStack - -FluidStack is defunct and no active credential remains. Make no FluidStack -provider changes as part of this work. - ---- - -### Task 6: Replace Nebius name inference with explicit platform metadata - -**Files:** - -- Modify: `v1/providers/nebius/instancetype.go` -- Create: `v1/providers/nebius/instancetype_test.go` - -- [ ] **Step 1: Write failing platform-map and filter tests** - -Create `v1/providers/nebius/instancetype_test.go`: - -```go -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" -) - -func TestNebiusPlatformArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - platform string - want cloudv1.Architecture - }{ - {platform: "gpu-b300-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-b200-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-b200-sxm-a", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-h200-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-h100-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-rtx6000", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-l40s-a", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-l40s-d", want: cloudv1.ArchitectureX86_64}, - {platform: "cpu-d3", want: cloudv1.ArchitectureX86_64}, - {platform: "cpu-e2", want: cloudv1.ArchitectureX86_64}, - {platform: "future-platform", want: cloudv1.ArchitectureUnknown}, - } - - for _, tt := range tests { - t.Run(tt.platform, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, nebiusPlatformArchitecture(tt.platform)) - }) - } -} - -func TestApplyInstanceTypeFiltersUsesArchitectureMetadata(t *testing.T) { - t.Parallel() - - x86Type := cloudv1.InstanceType{ - Type: "x86-type", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - } - armType := cloudv1.InstanceType{ - Type: "arm-type", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, - } - unknownType := cloudv1.InstanceType{ - Type: "unknown-type", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureUnknown}, - } - all := []cloudv1.InstanceType{x86Type, armType, unknownType} - - tests := []struct { - name string - args cloudv1.GetInstanceTypeArgs - want []cloudv1.InstanceType - }{ - {name: "unfiltered", want: all}, - { - name: "include x86", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - IncludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureX86_64, - }, - }, - }, - want: []cloudv1.InstanceType{x86Type}, - }, - { - name: "include ARM", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - IncludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - want: []cloudv1.InstanceType{armType}, - }, - { - name: "exclude ARM", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - want: []cloudv1.InstanceType{x86Type, unknownType}, - }, - { - name: "exclude x86", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureX86_64, - }, - }, - }, - want: []cloudv1.InstanceType{armType, unknownType}, - }, - } - - client := &NebiusClient{} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, client.applyInstanceTypeFilters(all, tt.args)) - }) - } -} -``` - -- [ ] **Step 2: Run the focused tests and confirm the red state** - -Run: - -```bash -go test ./v1/providers/nebius -run 'NebiusPlatformArchitecture|ApplyInstanceTypeFiltersUsesArchitectureMetadata' -count=1 -``` - -Expected: `nebiusPlatformArchitecture` is undefined, and exclusion cases expose -the current include-only, type-name-based behavior. - -- [ ] **Step 3: Add the explicit provider-local platform map** - -Add near the existing platform helpers: - -```go -var nebiusPlatformArchitectures = map[string]v1.Architecture{ - "gpu-b300-sxm": v1.ArchitectureX86_64, - "gpu-b200-sxm": v1.ArchitectureX86_64, - "gpu-b200-sxm-a": v1.ArchitectureX86_64, - "gpu-h200-sxm": v1.ArchitectureX86_64, - "gpu-h100-sxm": v1.ArchitectureX86_64, - "gpu-rtx6000": v1.ArchitectureX86_64, - "gpu-l40s-a": v1.ArchitectureX86_64, - "gpu-l40s-d": v1.ArchitectureX86_64, - "cpu-d3": v1.ArchitectureX86_64, - "cpu-e2": v1.ArchitectureX86_64, -} - -func nebiusPlatformArchitecture(platformName string) v1.Architecture { - architecture, ok := nebiusPlatformArchitectures[ - strings.ToLower(strings.TrimSpace(platformName)) - ] - if !ok { - return v1.ArchitectureUnknown - } - return architecture -} -``` - -Use exact platform IDs rather than GPU-generation inference. This is why B200 -and B300 map to x86. - -- [ ] **Step 4: Populate `SupportedArchitectures` at construction** - -In the `v1.InstanceType` literal inside `getInstanceTypesForLocation`, add: - -```go -SupportedArchitectures: []v1.Architecture{ - nebiusPlatformArchitecture(platform.Metadata.Name), -}, -``` - -- [ ] **Step 5: Replace custom architecture filtering** - -Add: - -```go -func supportsAllowedArchitecture( - instanceType v1.InstanceType, - filter *v1.ArchitectureFilter, -) bool { - for _, architecture := range instanceType.SupportedArchitectures { - if filter.IsAllowed(architecture) { - return true - } - } - return false -} -``` - -Replace the current architecture block in `applyInstanceTypeFilters` with: - -```go -if args.ArchitectureFilter != nil && - !supportsAllowedArchitecture(instanceType, args.ArchitectureFilter) { - continue -} -``` - -Delete `determineInstanceTypeArchitecture`. Do not add a synthetic ARM platform. - -- [ ] **Step 6: Format and run the complete Nebius package** - -Run: - -```bash -gofmt -w v1/providers/nebius/instancetype.go v1/providers/nebius/instancetype_test.go -go test ./v1/providers/nebius -count=1 -``` - -Expected: all Nebius tests pass. Unknown platforms remain present in an -unfiltered result and are handled according to the caller's filter. - -- [ ] **Step 7: Commit the provider change** - -```bash -git add v1/providers/nebius/instancetype.go v1/providers/nebius/instancetype_test.go -git commit -m "feat: report Nebius platform architectures" -``` - ---- - -### Task 7: Make the Nebius image catalog architecture-complete - -**Files:** - -- Modify: `v1/providers/nebius/image.go` -- Create: `v1/providers/nebius/image_test.go` - -- [ ] **Step 1: Write failing extraction and unfiltered-list tests** - -Create `v1/providers/nebius/image_test.go`: - -```go -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/require" - common "github.com/nebius/gosdk/proto/nebius/common/v1" - compute "github.com/nebius/gosdk/proto/nebius/compute/v1" - - cloudv1 "github.com/brevdev/cloud/v1" -) - -func TestExtractArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - image *compute.Image - want string - }{ - { - name: "authoritative AMD64 enum", - image: &compute.Image{ - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_AMD64, - }, - }, - want: string(cloudv1.ArchitectureX86_64), - }, - { - name: "authoritative ARM64 enum", - image: &compute.Image{ - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_ARM64, - }, - }, - want: string(cloudv1.ArchitectureARM64), - }, - { - name: "enum wins over legacy label", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{ - Labels: map[string]string{"architecture": "amd64"}, - }, - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_ARM64, - }, - }, - want: string(cloudv1.ArchitectureARM64), - }, - { - name: "legacy label is canonicalized", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{ - Labels: map[string]string{"arch": "aarch64"}, - }, - }, - want: string(cloudv1.ArchitectureARM64), - }, - { - name: "legacy name fallback", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{Name: "ubuntu-24.04-amd64"}, - }, - want: string(cloudv1.ArchitectureX86_64), - }, - { - name: "unknown stays unknown", - image: &compute.Image{}, - want: string(cloudv1.ArchitectureUnknown), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, extractArchitecture(tt.image)) - }) - } -} - -func TestApplyImageFiltersWithoutArchitectureReturnsAll(t *testing.T) { - t.Parallel() - - images := []cloudv1.Image{ - {ID: "amd64", Architecture: string(cloudv1.ArchitectureX86_64)}, - {ID: "arm64", Architecture: string(cloudv1.ArchitectureARM64)}, - {ID: "unknown", Architecture: string(cloudv1.ArchitectureUnknown)}, - } - - require.Equal( - t, - images, - applyImageFilters(images, cloudv1.GetImageArgs{}), - ) -} -``` - -- [ ] **Step 2: Run the focused tests and confirm the red state** - -Run: - -```bash -go test ./v1/providers/nebius -run 'ExtractArchitecture|ApplyImageFilters' -count=1 -``` - -Expected: the authoritative enum and unknown cases fail, and -`applyImageFilters` is undefined. - -- [ ] **Step 3: Make the provider enum authoritative** - -Replace `extractArchitecture` with: - -```go -func extractArchitecture(image *compute.Image) string { - if image != nil && image.Spec != nil { - switch image.Spec.CpuArchitecture { - case compute.ImageSpec_AMD64: - return string(v1.ArchitectureX86_64) - case compute.ImageSpec_ARM64: - return string(v1.ArchitectureARM64) - } - } - - if image != nil && image.Metadata != nil { - for _, label := range []string{"architecture", "arch"} { - rawArchitecture, ok := image.Metadata.Labels[label] - if !ok { - continue - } - architecture := v1.GetArchitecture(rawArchitecture) - if architecture != v1.ArchitectureUnknown { - return string(architecture) - } - } - - name := strings.ToLower(image.Metadata.Name) - if strings.Contains(name, ArchitectureArm64) || - strings.Contains(name, ArchitectureAArch64) { - return string(v1.ArchitectureARM64) - } - if strings.Contains(name, ArchitectureX86_64) || - strings.Contains(name, ArchitectureAMD64) { - return string(v1.ArchitectureX86_64) - } - } - - return string(v1.ArchitectureUnknown) -} -``` - -- [ ] **Step 4: Centralize and remove the default x86 filter** - -Add: - -```go -func applyImageFilters( - images []v1.Image, - args v1.GetImageArgs, -) []v1.Image { - images = filterImagesByArchitectures(images, args.Architectures) - return filterImagesByNameFilters(images, args.NameFilters) -} -``` - -In `GetImages`, delete the block that substitutes `[]string{"x86_64"}` when -`args.Architectures` is empty. Replace both filter blocks with: - -```go -return applyImageFilters(images, args), nil -``` - -In `getDefaultImages`, replace the hardcoded architecture with: - -```go -Architecture: extractArchitecture(image), -``` - -- [ ] **Step 5: Format and run the complete Nebius package** - -Run: - -```bash -gofmt -w v1/providers/nebius/image.go v1/providers/nebius/image_test.go -go test ./v1/providers/nebius -count=1 -``` - -Expected: all Nebius tests pass, and an empty architecture filter preserves -AMD64, ARM64, and unknown images. - -- [ ] **Step 6: Commit the image-catalog change** - -```bash -git add v1/providers/nebius/image.go v1/providers/nebius/image_test.go -git commit -m "feat: expose Nebius image architectures" -``` - ---- - -### Task 8: Record SFComputeV2's current x86-only provider contract - -**Files:** - -- Create: `v1/providers/sfcomputev2/instancetype_test.go` -- Verify only: `v1/providers/sfcomputev2/instancetype.go` - -- [ ] **Step 1: Add SFComputeV2 characterization and filter coverage** - -Create `v1/providers/sfcomputev2/instancetype_test.go`: - -```go -package v2 - -import ( - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" -) - -func TestSFComputeV2InstanceTypeArchitecture(t *testing.T) { - t.Parallel() - - instanceType := buildInstanceType(h100InstanceTypeMetadata, true) - require.Equal( - t, - []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - instanceType.SupportedArchitectures, - ) - require.False( - t, - cloudv1.IsSelectedByArgs( - instanceType, - cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - IncludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - ), - ) -} -``` - -- [ ] **Step 2: Run the characterization package** - -Run: - -```bash -gofmt -w v1/providers/sfcomputev2/instancetype_test.go -go test ./v1/providers/sfcomputev2 -count=1 -``` - -Expected: tests pass without production changes. H100 remains truthfully x86 -until SFComputeV2 exposes an authoritative Grace SKU and compatible image. - -- [ ] **Step 3: Commit the contract test** - -```bash -git add v1/providers/sfcomputev2/instancetype_test.go -git commit -m "test: record SFComputeV2 architecture contract" -``` - ---- - -### Task 9: Run cross-provider verification and inspect the final diff - -**Files:** - -- Verify all files changed by Tasks 1 through 8 - -- [ ] **Step 1: Format every touched Go file** - -Run: - -```bash -gofmt -w \ - v1/instancetype.go \ - v1/instancetype_test.go \ - v1/providers/launchpad/instancetype_test.go \ - v1/providers/shadeform/instancetype.go \ - v1/providers/shadeform/instancetype_test.go \ - v1/providers/lambdalabs/instancetype.go \ - v1/providers/lambdalabs/instancetype_test.go \ - v1/providers/nebius/instancetype.go \ - v1/providers/nebius/instancetype_test.go \ - v1/providers/nebius/image.go \ - v1/providers/nebius/image_test.go \ - v1/providers/sfcomputev2/instancetype_test.go -``` - -- [ ] **Step 2: Run the focused cross-provider suite** - -Run: - -```bash -go test \ - ./v1 \ - ./v1/providers/launchpad \ - ./v1/providers/shadeform \ - ./v1/providers/lambdalabs \ - ./v1/providers/nebius \ - ./v1/providers/sfcomputev2 \ - -count=1 -``` - -Expected: all focused packages pass. - -- [ ] **Step 3: Run repository-wide tests** - -Run: - -```bash -go test ./v1/... -count=1 -go test ./... -count=1 -``` - -Expected: both commands pass. Investigate any failure before claiming completion; -do not dismiss provider failures without reproducing the exact package. - -- [ ] **Step 4: Run formatting and lint checks** - -Run: - -```bash -make fmt-check -golangci-lint run ./... -``` - -Expected: both commands pass. If `golangci-lint` is not installed, use the -repository-pinned version from `Makefile` rather than changing tool versions. - -- [ ] **Step 5: Verify scope and semantics in the diff** - -Run: - -```bash -git diff --check -git status --short -git diff --stat HEAD~8..HEAD -git diff HEAD~8..HEAD -- \ - v1/instancetype.go \ - v1/providers/launchpad \ - v1/providers/shadeform \ - v1/providers/lambdalabs \ - v1/providers/nebius \ - v1/providers/sfcomputev2 -``` - -Confirm: - -- no dev-plane or Verb files changed; -- no generated clients changed; -- Launchpad still uses `SystemArch`; -- Shadeform and LambdaLabs share only Grace recognition; -- FluidStack remains unchanged; -- SFCompute remains unchanged; -- SFComputeV2 still reports its existing x86-only contract; -- Nebius unknown platforms and images remain `unknown`; -- no synthetic Nebius or TestKube ARM offering was introduced; -- ARM provisioning and boot-image selection remain unchanged. - -- [ ] **Step 6: Commit any verification-only formatting adjustment** - -Only if Step 1 changed a file after its task commit: - -```bash -git add v1 -git commit -m "style: format ARM catalog changes" -``` - -If `git status --short` is empty, do not create an empty commit. diff --git a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md b/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md deleted file mode 100644 index f0b984a..0000000 --- a/docs/superpowers/specs/2026-07-27-cloud-arm-instance-catalog-design.md +++ /dev/null @@ -1,207 +0,0 @@ -# Cloud ARM Instance Catalog Design - -## Goal - -Make the Cloud SDK's instance and image catalogs architecture-complete without -changing dev-plane's current x86-only policy. - -Cloud providers must: - -- return every known instance type when no architecture filter is supplied; -- populate `SupportedArchitectures` with truthful canonical values; -- honor a caller-provided architecture filter; -- report an unknown architecture as `unknown` instead of silently treating it - as `x86_64`. - -This preserves the intended boundary: - -```text -Cloud reports provider capabilities. -Callers decide which capabilities their product currently supports. -``` - -Dev-plane will remain unchanged. Its current request excludes ARM, so ARM -instance types added by this change will not pass into its synchronized catalog. - -## Scope - -### Shared architecture classification - -Add a small, case-insensitive helper that recognizes NVIDIA Grace products from -their GPU model: - -- `GH*` is ARM64. -- `GB*` is ARM64. -- A plain `B*` model does not imply ARM64. - -The helper should answer only whether a GPU model identifies a Grace system. -Each provider remains responsible for choosing the fallback architecture based -on its own catalog contract. - -Use canonical Cloud values: - -- `x86_64` -- `arm64` -- `unknown` - -### Launchpad - -Launchpad already supplies an authoritative `SystemArch` field and maps it to -the Cloud architecture values. No production mapping change is needed. - -Add unit coverage proving: - -- `SystemArchAMD64` becomes `x86_64`; -- `SystemArchARM64` becomes `arm64`; -- caller-provided ARM exclusions remove ARM catalog entries. - -### Shadeform - -Retain the existing Grace inference while moving it to the shared helper. -Normalize model casing before classification. - -Add conversion coverage for: - -- GH200 as ARM64; -- GB200 as ARM64; -- lowercase Grace model names as ARM64; -- H100 and B200 as x86_64. - -### LambdaLabs - -Replace the unconditional x86 assignment with architecture derived from the -parsed GPU model: - -- recognized Grace products become ARM64; -- other currently supported Lambda instance types remain x86_64. - -The existing architecture filter remains authoritative and must exclude a -GH200 type when ARM is excluded. - -### FluidStack - -FluidStack is defunct and no active credential remains. Leave its provider -adapter unchanged and exclude it from this work. - -### Nebius instance types - -Nebius does not currently expose an architecture field in its Platform API. -Use a provider-local mapping for the platform IDs that Cloud supports. - -The currently documented Nebius platforms use Intel or AMD host CPUs, including -the B200 and B300 platforms, and must therefore report `x86_64`. An unrecognized -platform must report `unknown`. - -Populate `SupportedArchitectures` during instance-type construction. Replace -the current type-name inference and include-only filtering with filtering over -the populated architecture metadata using `ArchitectureFilter.IsAllowed`, so -both include and exclude filters work. - -Do not invent a synthetic Nebius ARM platform. Add a real ARM mapping only when -Nebius publishes an ARM platform identifier or exposes an authoritative -architecture field. - -### Nebius images - -Remove the catalog-level x86 restriction: - -- an unfiltered image request returns all architectures; -- use the Nebius `ImageSpec.CpuArchitecture` enum as the authoritative source; -- retain labels and image-name parsing only as compatibility fallbacks; -- use `unknown` when no source identifies the architecture. - -This changes image discovery only. Architecture-aware boot-image selection is -part of the later provisioning work. - -### SFCompute - -SFCompute is deprecated in favor of SFComputeV2. Leave the v1 provider -unchanged and exclude it from this work. - -### SFComputeV2 - -Make no production classification change. The current provider exposes an H100 -offering backed by x86-only image requirements, and its metadata already -reports `x86_64`. - -Add focused regression coverage where practical so this is recorded as a -provider capability rather than an accidental ARM restriction. - -Future SFComputeV2 ARM support requires an authoritative Grace SKU plus -architecture-compatible image and create-path support; it is not part of this -catalog change. - -### TestKube - -Defer ARM advertising. - -Safe TestKube ARM support requires: - -- a separately named ARM instance type; -- `SupportedArchitectures: [arm64]`; -- a `kubernetes.io/arch: arm64` pod node selector; -- a published and verified `linux/arm64` image manifest. - -The existing `test.ok.cpu` type remains x86-only. A single type advertising -both architectures would be unsafe because it could pass an x86 filter and -then schedule onto an ARM node. - -## Data Flow - -For Cloud callers: - -```text -provider API response - -> provider-specific architecture extraction - -> canonical Cloud architecture - -> optional ArchitectureFilter - -> returned InstanceType or Image -``` - -An unfiltered call returns x86, ARM, and unknown entries. A filtered call -returns only entries whose architecture is allowed. dev-plane will continue to -send its existing x86-only filter and therefore will not receive ARM entries. - -## Error and Unknown-Data Handling - -- Do not fail an entire provider catalog because one item has an unknown - architecture. -- Preserve that item as `unknown` when the provider otherwise supports it. -- Never default an unrecognized provider platform or image to x86. -- Continue returning provider/API errors using the existing error-wrapping - conventions. - -## Testing - -Follow test-first development for every behavior change. - -Focused tests will cover: - -- shared Grace GPU classification, including casing and B200 non-ARM behavior; -- Launchpad authoritative AMD64 and ARM64 conversion; -- Shadeform GH/GB conversion; -- LambdaLabs GH200 conversion and architecture filtering; -- Nebius known x86 and unknown platform mappings; -- Nebius include and exclude filter behavior; -- Nebius authoritative AMD64/ARM64 image metadata and unfiltered image listing; -- existing SFComputeV2 x86 metadata where a focused unit seam exists. - -Run package-level tests while iterating, then: - -```bash -go test ./v1/... -go test ./... -``` - -Run `gofmt` on every touched Go file and run the repository's configured -linting if practical. - -## Non-Goals - -- No dev-plane changes. -- No removal of the public `ArchitectureFilter` contract. -- No enabling ARM instance creation in dev-plane. -- No architecture-aware provider boot-image selection beyond image catalog - metadata. -- No TestKube image publishing or ARM scheduling change. -- No Verb-mode changes. diff --git a/v1/instancetype.go b/v1/instancetype.go index 2dead39..a58bc0b 100644 --- a/v1/instancetype.go +++ b/v1/instancetype.go @@ -52,13 +52,6 @@ func GetArchitecture(architecture string) Architecture { } } -func IsNVIDIAGraceGPU(gpuModel string) bool { - normalizedModel := strings.ToUpper(strings.TrimSpace(gpuModel)) - normalizedModel = strings.TrimPrefix(normalizedModel, "NVIDIA ") - return strings.HasPrefix(normalizedModel, "GH") || - strings.HasPrefix(normalizedModel, "GB") -} - type InstanceTypeID string type InstanceType struct { diff --git a/v1/instancetype_test.go b/v1/instancetype_test.go index 4e43d23..d76b323 100644 --- a/v1/instancetype_test.go +++ b/v1/instancetype_test.go @@ -28,31 +28,3 @@ func TestGetArchitectureAliases(t *testing.T) { }) } } - -func TestIsNVIDIAGraceGPU(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - gpuModel string - want bool - }{ - {name: "GH200", gpuModel: "GH200", want: true}, - {name: "GB200", gpuModel: "GB200", want: true}, - {name: "lowercase GH200", gpuModel: "gh200", want: true}, - {name: "trimmed GB200", gpuModel: " gb200 ", want: true}, - {name: "vendor-prefixed GH200", gpuModel: "NVIDIA GH200", want: true}, - {name: "H100", gpuModel: "H100", want: false}, - {name: "B200", gpuModel: "B200", want: false}, - {name: "empty", gpuModel: "", want: false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if got := IsNVIDIAGraceGPU(tt.gpuModel); got != tt.want { - t.Fatalf("IsNVIDIAGraceGPU(%q) = %v, want %v", tt.gpuModel, got, tt.want) - } - }) - } -} diff --git a/v1/providers/launchpad/instancetype_test.go b/v1/providers/launchpad/instancetype_test.go index a959d4d..43822f3 100644 --- a/v1/providers/launchpad/instancetype_test.go +++ b/v1/providers/launchpad/instancetype_test.go @@ -8,7 +8,6 @@ import ( "github.com/brevdev/cloud/internal/validation" v1 "github.com/brevdev/cloud/v1" - openapi "github.com/brevdev/cloud/v1/providers/launchpad/gen/launchpad" ) func TestGetInstanceTypes(t *testing.T) { @@ -131,41 +130,3 @@ func TestMakeGenericInstanceTypeID(t *testing.T) { }) } } - -func TestLaunchpadArchitectureToArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - raw openapi.SystemArchEnum - want v1.Architecture - }{ - {name: "AMD64", raw: openapi.SystemArchAMD64, want: v1.ArchitectureX86_64}, - {name: "ARM64", raw: openapi.SystemArchARM64, want: v1.ArchitectureARM64}, - {name: "unknown", raw: openapi.SystemArchEnum("riscv64"), want: v1.ArchitectureUnknown}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, launchpadArchitectureToArchitecture(tt.raw)) - }) - } -} - -func TestLaunchpadArchitectureFilterExcludesARM(t *testing.T) { - t.Parallel() - - instanceType := v1.InstanceType{ - SupportedArchitectures: []v1.Architecture{ - launchpadArchitectureToArchitecture(openapi.SystemArchARM64), - }, - } - args := v1.GetInstanceTypeArgs{ - ArchitectureFilter: &v1.ArchitectureFilter{ - ExcludeArchitectures: []v1.Architecture{v1.ArchitectureARM64}, - }, - } - - require.False(t, v1.IsSelectedByArgs(instanceType, args)) -} diff --git a/v1/providers/nebius/client.go b/v1/providers/nebius/client.go index 85aa088..397b988 100644 --- a/v1/providers/nebius/client.go +++ b/v1/providers/nebius/client.go @@ -26,9 +26,6 @@ type NebiusClient struct { location string sdk *gosdk.SDK logger v1.Logger - - listImages listNebiusImagesFunc - getInstanceTypePrice getInstanceTypePriceFunc } var _ v1.CloudClient = &NebiusClient{} diff --git a/v1/providers/nebius/image.go b/v1/providers/nebius/image.go index eabfef3..ff18b13 100644 --- a/v1/providers/nebius/image.go +++ b/v1/providers/nebius/image.go @@ -9,17 +9,7 @@ import ( compute "github.com/nebius/gosdk/proto/nebius/compute/v1" ) -type listNebiusImagesFunc func(context.Context) []v1.Image - func (c *NebiusClient) GetImages(ctx context.Context, args v1.GetImageArgs) ([]v1.Image, error) { - listImages := c.listImages - if listImages == nil { - listImages = c.listImagesFromProvider - } - return applyImageFilters(listImages(ctx), args), nil -} - -func (c *NebiusClient) listImagesFromProvider(ctx context.Context) []v1.Image { var images []v1.Image // First, try to get project-specific images @@ -42,7 +32,7 @@ func (c *NebiusClient) listImagesFromProvider(ctx context.Context) []v1.Image { } } - return images + return applyImageFilters(images, args), nil } // getProjectImages retrieves images specific to the current project diff --git a/v1/providers/nebius/image_test.go b/v1/providers/nebius/image_test.go index c0bc873..2391957 100644 --- a/v1/providers/nebius/image_test.go +++ b/v1/providers/nebius/image_test.go @@ -1,7 +1,6 @@ package v1 import ( - "context" "testing" common "github.com/nebius/gosdk/proto/nebius/common/v1" @@ -14,153 +13,49 @@ import ( func TestExtractArchitecture(t *testing.T) { t.Parallel() + image := func( + architecture compute.ImageSpec_CPUArchitecture, + labels map[string]string, + ) *compute.Image { + return &compute.Image{ + Metadata: &common.ResourceMetadata{Labels: labels}, + Spec: &compute.ImageSpec{CpuArchitecture: architecture}, + } + } + tests := []struct { name string image *compute.Image - want string + want cloudv1.Architecture }{ + {name: "AMD64 enum", image: image(compute.ImageSpec_AMD64, nil), want: cloudv1.ArchitectureX86_64}, { - name: "authoritative AMD64 enum", - image: &compute.Image{ - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_AMD64, - }, - }, - want: string(cloudv1.ArchitectureX86_64), - }, - { - name: "authoritative ARM64 enum", - image: &compute.Image{ - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_ARM64, - }, - }, - want: string(cloudv1.ArchitectureARM64), - }, - { - name: "enum wins over legacy label", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{ - Labels: map[string]string{"architecture": "amd64"}, - }, - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_ARM64, - }, - }, - want: string(cloudv1.ArchitectureARM64), - }, - { - name: "unknown nonzero enum does not use legacy metadata", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{ - Name: "ubuntu-24.04-arm64", - Labels: map[string]string{"architecture": "amd64"}, - }, - Spec: &compute.ImageSpec{ - CpuArchitecture: compute.ImageSpec_CPUArchitecture(99), - }, - }, - want: string(cloudv1.ArchitectureUnknown), - }, - { - name: "legacy label is canonicalized", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{ - Labels: map[string]string{"arch": "aarch64"}, - }, - }, - want: string(cloudv1.ArchitectureARM64), - }, - { - name: "legacy name fallback", - image: &compute.Image{ - Metadata: &common.ResourceMetadata{Name: "ubuntu-24.04-amd64"}, - }, - want: string(cloudv1.ArchitectureX86_64), - }, - { - name: "unknown stays unknown", - image: &compute.Image{}, - want: string(cloudv1.ArchitectureUnknown), - }, + name: "ARM64 enum wins over legacy metadata", + image: image( + compute.ImageSpec_ARM64, + map[string]string{"architecture": "amd64"}, + ), + want: cloudv1.ArchitectureARM64, + }, + {name: "legacy label", image: image(compute.ImageSpec_UNSPECIFIED, map[string]string{"arch": "aarch64"}), want: cloudv1.ArchitectureARM64}, + {name: "unknown enum", image: image(compute.ImageSpec_CPUArchitecture(99), map[string]string{"architecture": "amd64"}), want: cloudv1.ArchitectureUnknown}, + {name: "unknown", image: &compute.Image{}, want: cloudv1.ArchitectureUnknown}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - require.Equal(t, tt.want, extractArchitecture(tt.image)) + require.Equal(t, string(tt.want), extractArchitecture(tt.image)) }) } } -func TestApplyImageFiltersWithoutArchitectureReturnsAll(t *testing.T) { +func TestApplyImageFiltersDoesNotDefaultToX86(t *testing.T) { t.Parallel() - images := []cloudv1.Image{ - {ID: "amd64", Architecture: string(cloudv1.ArchitectureX86_64)}, - {ID: "arm64", Architecture: string(cloudv1.ArchitectureARM64)}, - {ID: "unknown", Architecture: string(cloudv1.ArchitectureUnknown)}, - } - - require.Equal( - t, - images, - applyImageFilters(images, cloudv1.GetImageArgs{}), - ) -} - -func TestNebiusClientGetImagesAppliesArchitectureFilter(t *testing.T) { - t.Parallel() + x86 := cloudv1.Image{ID: "x86", Architecture: string(cloudv1.ArchitectureX86_64)} + arm := cloudv1.Image{ID: "arm", Architecture: string(cloudv1.ArchitectureARM64)} + images := []cloudv1.Image{x86, arm} - client := &NebiusClient{ - listImages: func(context.Context) []cloudv1.Image { - return []cloudv1.Image{ - { - ID: "arm-image", - Name: "arm-image", - Architecture: string(cloudv1.ArchitectureARM64), - }, - { - ID: "x86-image", - Name: "x86-image", - Architecture: string(cloudv1.ArchitectureX86_64), - }, - } - }, - } - - unfiltered, err := client.GetImages(context.Background(), cloudv1.GetImageArgs{}) - require.NoError(t, err) - require.Equal( - t, - []cloudv1.Image{ - { - ID: "arm-image", - Name: "arm-image", - Architecture: string(cloudv1.ArchitectureARM64), - }, - { - ID: "x86-image", - Name: "x86-image", - Architecture: string(cloudv1.ArchitectureX86_64), - }, - }, - unfiltered, - ) - - filtered, err := client.GetImages(context.Background(), cloudv1.GetImageArgs{ - Architectures: []string{string(cloudv1.ArchitectureX86_64)}, - }) - require.NoError(t, err) - require.Equal( - t, - []cloudv1.Image{ - { - ID: "x86-image", - Name: "x86-image", - Architecture: string(cloudv1.ArchitectureX86_64), - }, - }, - filtered, - ) + require.Equal(t, images, applyImageFilters(images, cloudv1.GetImageArgs{})) } diff --git a/v1/providers/nebius/instance_test.go b/v1/providers/nebius/instance_test.go index 4b10964..ee1c8f7 100644 --- a/v1/providers/nebius/instance_test.go +++ b/v1/providers/nebius/instance_test.go @@ -356,32 +356,31 @@ func TestIsPlatformSupported(t *testing.T) { shouldSupport bool description string }{ - // Exact documented GPU platforms are supported. - {"gpu-b300-sxm", true, "B300 with gpu prefix"}, + // GPU platforms - all should be supported {"gpu-h100-sxm", true, "H100 with gpu prefix"}, {"gpu-h200-sxm", true, "H200 with gpu prefix"}, {"gpu-b200-sxm", true, "B200 with gpu prefix"}, - {"gpu-b200-sxm-a", true, "B200 alternate platform"}, - {"gpu-rtx6000", true, "RTX PRO 6000 platform"}, - {"gpu-l40s-a", true, "L40S Intel platform"}, - {"gpu-l40s-d", true, "L40S AMD platform"}, + {"gpu-b300-sxm", true, "B300 with gpu prefix"}, + {"gpu-rtx6000", true, "RTX 6000 with gpu prefix"}, + {"gpu-l40s", true, "L40S with gpu prefix"}, + {"gpu-a100-sxm4", true, "A100 with gpu prefix"}, + {"gpu-v100-sxm2", true, "V100 with gpu prefix"}, + {"gpu-a10", true, "A10 with gpu prefix"}, + {"gpu-t4", true, "T4 with gpu prefix"}, + {"gpu-l4", true, "L4 with gpu prefix"}, + + // GPU platforms without "gpu-" prefix (B200 specific test) + {"b200-sxm", true, "B200 without gpu prefix"}, + {"b200", true, "B200 bare name"}, + {"h100-sxm", true, "H100 without gpu prefix"}, + {"l40s", true, "L40S without gpu prefix"}, // CPU platforms - only specific ones supported {"cpu-d3", true, "CPU D3 platform"}, {"cpu-e2", true, "CPU E2 platform"}, {"cpu-other", false, "Unsupported CPU platform"}, - // Legacy aliases and undocumented platforms are unsupported. - {"gpu-l40s", false, "L40S alias without documented suffix"}, - {"gpu-a100-sxm4", false, "A100 legacy platform"}, - {"gpu-v100-sxm2", false, "V100 legacy platform"}, - {"gpu-a10", false, "A10 legacy platform"}, - {"gpu-t4", false, "T4 legacy platform"}, - {"gpu-l4", false, "L4 legacy platform"}, - {"b200-sxm", false, "B200 without gpu prefix"}, - {"b200", false, "B200 bare name"}, - {"h100-sxm", false, "H100 without gpu prefix"}, - {"l40s", false, "L40S without gpu prefix"}, + // Unsupported platforms {"unknown-platform", false, "Generic unknown platform"}, {"random-gpu", false, "Random name with gpu"}, } diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index 7f520eb..8890321 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -17,13 +17,6 @@ import ( quotas "github.com/nebius/gosdk/proto/nebius/quotas/v1" ) -type getInstanceTypePriceFunc func( - context.Context, - string, - string, - string, -) *currency.Amount - func (c *NebiusClient) GetInstanceTypes(ctx context.Context, args v1.GetInstanceTypeArgs) ([]v1.InstanceType, error) { // Get platforms (instance types) from Nebius API platformsResp, err := c.sdk.Services().Compute().V1().Platform().List(ctx, &compute.ListPlatformsRequest{ @@ -222,12 +215,7 @@ func (c *NebiusClient) getInstanceTypesForLocation(ctx context.Context, platform } // Enrich with pricing information from Nebius Billing API - pricing := c.getPriceForInstanceType( - ctx, - platform.Metadata.Name, - preset.Name, - location.Name, - ) + pricing := c.getPricingForInstanceType(ctx, platform.Metadata.Name, preset.Name, location.Name) if pricing != nil { instanceType.BasePrice = pricing } @@ -475,8 +463,25 @@ func nebiusPlatformArchitecture(platformName string) v1.Architecture { // isPlatformSupported checks if a platform should be included in instance types func (c *NebiusClient) isPlatformSupported(platformName string) bool { - _, ok := nebiusPlatformArchitectures[strings.ToLower(strings.TrimSpace(platformName))] - return ok + platformLower := strings.ToLower(platformName) + + // For GPU platforms: only accept known GPU types + // Check for specific GPU model names (with or without "gpu-" prefix) + knownGPUTypes := []string{ + "h100", "h200", "l40s", "a100", "v100", "a10", "t4", "l4", "b200", "b300", "rtx6000", + } + for _, gpuType := range knownGPUTypes { + if strings.Contains(platformLower, gpuType) { + return true + } + } + + // For CPU platforms: only accept specific types to avoid polluting the list + if strings.Contains(platformLower, "cpu-d3") || strings.Contains(platformLower, "cpu-e2") { + return true + } + + return false } // isCPUOnlyPlatform checks if a platform is CPU-only (no GPUs) @@ -609,18 +614,6 @@ func getGPUMemory(gpuType string) units.Base2Bytes { return units.Base2Bytes(0) } -func (c *NebiusClient) getPriceForInstanceType( - ctx context.Context, - platformName string, - presetName string, - location string, -) *currency.Amount { - if c.getInstanceTypePrice != nil { - return c.getInstanceTypePrice(ctx, platformName, presetName, location) - } - return c.getPricingForInstanceType(ctx, platformName, presetName, location) -} - // getPricingForInstanceType fetches real pricing from Nebius Billing Calculator API // Returns nil if pricing cannot be fetched (non-critical failure) func (c *NebiusClient) getPricingForInstanceType(ctx context.Context, platformName, presetName, _ string) *currency.Amount { diff --git a/v1/providers/nebius/instancetype_test.go b/v1/providers/nebius/instancetype_test.go index ecf8032..87f1ffe 100644 --- a/v1/providers/nebius/instancetype_test.go +++ b/v1/providers/nebius/instancetype_test.go @@ -1,13 +1,8 @@ package v1 import ( - "context" "testing" - "github.com/alecthomas/units" - "github.com/bojanz/currency" - common "github.com/nebius/gosdk/proto/nebius/common/v1" - compute "github.com/nebius/gosdk/proto/nebius/compute/v1" "github.com/stretchr/testify/require" cloudv1 "github.com/brevdev/cloud/v1" @@ -16,208 +11,40 @@ import ( func TestNebiusPlatformArchitecture(t *testing.T) { t.Parallel() - tests := []struct { - platform string - want cloudv1.Architecture - }{ - {platform: "gpu-b300-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-b200-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-b200-sxm-a", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-h200-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-h100-sxm", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-rtx6000", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-l40s-a", want: cloudv1.ArchitectureX86_64}, - {platform: "gpu-l40s-d", want: cloudv1.ArchitectureX86_64}, - {platform: "cpu-d3", want: cloudv1.ArchitectureX86_64}, - {platform: "cpu-e2", want: cloudv1.ArchitectureX86_64}, - {platform: "future-platform", want: cloudv1.ArchitectureUnknown}, - } - - for _, tt := range tests { - t.Run(tt.platform, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, nebiusPlatformArchitecture(tt.platform)) - }) - } -} - -func TestGetInstanceTypesForLocationConstructsMappedPlatforms(t *testing.T) { - t.Parallel() - - client := &NebiusClient{ - projectID: "test-project", - logger: &cloudv1.NoopLogger{}, - getInstanceTypePrice: func( - context.Context, - string, - string, - string, - ) *currency.Amount { - return nil - }, - } - platforms := &compute.ListPlatformsResponse{ - Items: []*compute.Platform{ - { - Metadata: &common.ResourceMetadata{Name: "gpu-b300-sxm"}, - Spec: &compute.PlatformSpec{ - GpuMemoryGigabytes: 270, - Presets: []*compute.Preset{ - { - Name: "1gpu-24vcpu-346gb", - Resources: &compute.PresetResources{ - GpuCount: 1, - VcpuCount: 24, - MemoryGibibytes: 346, - }, - }, - }, - }, - }, - { - Metadata: &common.ResourceMetadata{Name: "gpu-rtx6000"}, - Spec: &compute.PlatformSpec{ - GpuMemoryGigabytes: 96, - Presets: []*compute.Preset{ - { - Name: "1gpu-24vcpu-218gb", - Resources: &compute.PresetResources{ - GpuCount: 1, - VcpuCount: 24, - MemoryGibibytes: 218, - }, - }, - }, - }, - }, - }, - } - - instanceTypes, err := client.getInstanceTypesForLocation( - context.Background(), - platforms, - cloudv1.Location{Name: "test-region"}, - cloudv1.GetInstanceTypeArgs{}, - nil, - nil, - ) - require.NoError(t, err) - require.Len(t, instanceTypes, 2) - - tests := []struct { - instanceType string - gpuType string - memoryGB int64 - }{ - { - instanceType: "gpu-b300-sxm.1gpu-24vcpu-346gb", - gpuType: "B300", - memoryGB: 270, - }, - { - instanceType: "gpu-rtx6000.1gpu-24vcpu-218gb", - gpuType: "RTX6000", - memoryGB: 96, - }, - } - for i, tt := range tests { - instanceType := instanceTypes[i] - require.Equal(t, tt.instanceType, instanceType.Type) - require.Equal( - t, - []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - instanceType.SupportedArchitectures, - ) - require.False(t, instanceType.IsAvailable) - require.Len(t, instanceType.SupportedGPUs, 1) - - gpu := instanceType.SupportedGPUs[0] - require.Equal(t, int32(1), gpu.Count) - require.Equal(t, tt.gpuType, gpu.Type) - require.Equal(t, tt.gpuType, gpu.Name) - require.Equal(t, cloudv1.ManufacturerNVIDIA, gpu.Manufacturer) - require.Equal( - t, - units.Base2Bytes(tt.memoryGB*int64(units.Gibibyte)), - gpu.Memory, - ) - } + require.Equal(t, cloudv1.ArchitectureX86_64, nebiusPlatformArchitecture(" GPU-H100-SXM ")) + require.Equal(t, cloudv1.ArchitectureX86_64, nebiusPlatformArchitecture("cpu-d3")) + require.Equal(t, cloudv1.ArchitectureUnknown, nebiusPlatformArchitecture("future-platform")) } func TestApplyInstanceTypeFiltersUsesArchitectureMetadata(t *testing.T) { t.Parallel() - x86Type := cloudv1.InstanceType{ - Type: "x86-type", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - } - armType := cloudv1.InstanceType{ - Type: "arm-type", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, + instanceType := func(name string, architecture cloudv1.Architecture) cloudv1.InstanceType { + return cloudv1.InstanceType{ + Type: name, + SupportedArchitectures: []cloudv1.Architecture{architecture}, + } } - unknownType := cloudv1.InstanceType{ - Type: "unknown-type", - SupportedArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureUnknown}, + x86 := instanceType("arm-in-name", cloudv1.ArchitectureX86_64) + arm := instanceType("x86-in-name", cloudv1.ArchitectureARM64) + unknown := instanceType("unknown", cloudv1.ArchitectureUnknown) + instanceTypes := []cloudv1.InstanceType{x86, arm, unknown} + client := &NebiusClient{} + apply := func(filter *cloudv1.ArchitectureFilter) []cloudv1.InstanceType { + return client.applyInstanceTypeFilters( + instanceTypes, + cloudv1.GetInstanceTypeArgs{ArchitectureFilter: filter}, + ) } - all := []cloudv1.InstanceType{x86Type, armType, unknownType} - tests := []struct { - name string - args cloudv1.GetInstanceTypeArgs - want []cloudv1.InstanceType - }{ - {name: "unfiltered", want: all}, - { - name: "include x86", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - IncludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureX86_64, - }, - }, - }, - want: []cloudv1.InstanceType{x86Type}, - }, - { - name: "include ARM", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - IncludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - want: []cloudv1.InstanceType{armType}, + require.Equal(t, []cloudv1.InstanceType{arm}, apply( + &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, }, - { - name: "exclude ARM", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - want: []cloudv1.InstanceType{x86Type, unknownType}, + )) + require.Equal(t, []cloudv1.InstanceType{x86, unknown}, apply( + &cloudv1.ArchitectureFilter{ + ExcludeArchitectures: []cloudv1.Architecture{cloudv1.ArchitectureARM64}, }, - { - name: "exclude x86", - args: cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - ExcludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureX86_64, - }, - }, - }, - want: []cloudv1.InstanceType{armType, unknownType}, - }, - } - - client := &NebiusClient{} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, client.applyInstanceTypeFilters(all, tt.args)) - }) - } + )) } diff --git a/v1/providers/sfcomputev2/instancetype_test.go b/v1/providers/sfcomputev2/instancetype_test.go deleted file mode 100644 index 7c5050a..0000000 --- a/v1/providers/sfcomputev2/instancetype_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package v2 - -import ( - "testing" - - "github.com/stretchr/testify/require" - - cloudv1 "github.com/brevdev/cloud/v1" -) - -func TestSFComputeV2InstanceTypeArchitecture(t *testing.T) { - t.Parallel() - - instanceType := buildInstanceType(h100InstanceTypeMetadata, true) - require.Equal( - t, - []cloudv1.Architecture{cloudv1.ArchitectureX86_64}, - instanceType.SupportedArchitectures, - ) - require.False( - t, - cloudv1.IsSelectedByArgs( - instanceType, - cloudv1.GetInstanceTypeArgs{ - ArchitectureFilter: &cloudv1.ArchitectureFilter{ - IncludeArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureARM64, - }, - }, - }, - ), - ) -} diff --git a/v1/providers/shadeform/instancetype.go b/v1/providers/shadeform/instancetype.go index 3cd32a8..06b0701 100644 --- a/v1/providers/shadeform/instancetype.go +++ b/v1/providers/shadeform/instancetype.go @@ -287,7 +287,8 @@ func shadeformCloud(cloud string) string { } func shadeformArchitecture(gpuName string) v1.Architecture { - if v1.IsNVIDIAGraceGPU(gpuName) { + // Shadeform currently does not specify the architecture, so we need to infer it from the GPU name. + if strings.HasPrefix(gpuName, "GH") || strings.HasPrefix(gpuName, "GB") { return v1.ArchitectureARM64 } return v1.ArchitectureX86_64 diff --git a/v1/providers/shadeform/instancetype_test.go b/v1/providers/shadeform/instancetype_test.go index c230304..27e074b 100644 --- a/v1/providers/shadeform/instancetype_test.go +++ b/v1/providers/shadeform/instancetype_test.go @@ -9,29 +9,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestShadeformArchitecture(t *testing.T) { - t.Parallel() - - tests := []struct { - gpuModel string - want v1.Architecture - }{ - {gpuModel: "GH200", want: v1.ArchitectureARM64}, - {gpuModel: "GB200", want: v1.ArchitectureARM64}, - {gpuModel: "gh200", want: v1.ArchitectureARM64}, - {gpuModel: "gb200", want: v1.ArchitectureARM64}, - {gpuModel: "H100", want: v1.ArchitectureX86_64}, - {gpuModel: "B200", want: v1.ArchitectureX86_64}, - } - - for _, tt := range tests { - t.Run(tt.gpuModel, func(t *testing.T) { - t.Parallel() - assert.Equal(t, tt.want, shadeformArchitecture(tt.gpuModel)) - }) - } -} - func TestIsSelectedByArgs(t *testing.T) { t.Parallel() From f3fb92e9ea6bbc1317d0bb022f69508098f2d1e5 Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Wed, 29 Jul 2026 09:01:46 -0700 Subject: [PATCH 15/17] feat: add ARM64 TestKube instance type --- v1/providers/nebius/image.go | 2 +- v1/providers/testkube/README.md | 6 +- .../testkube/images/ubuntu-vm/README.md | 21 ++----- v1/providers/testkube/instance.go | 1 + v1/providers/testkube/instance_test.go | 60 +++++++++++++++++++ v1/providers/testkube/instancetype.go | 42 +++++++++---- v1/providers/testkube/instancetype_test.go | 48 ++++++++++++++- 7 files changed, 148 insertions(+), 32 deletions(-) diff --git a/v1/providers/nebius/image.go b/v1/providers/nebius/image.go index ff18b13..18c1e02 100644 --- a/v1/providers/nebius/image.go +++ b/v1/providers/nebius/image.go @@ -197,7 +197,7 @@ const ( ArchitectureAArch64 = "aarch64" ) -// extractArchitecture extracts architecture information from image metadata. +// extractArchitecture extracts architecture information from image metadata func extractArchitecture(image *compute.Image) string { if image != nil && image.Spec != nil { switch image.Spec.CpuArchitecture { diff --git a/v1/providers/testkube/README.md b/v1/providers/testkube/README.md index 15b0c46..3ed1c46 100644 --- a/v1/providers/testkube/README.md +++ b/v1/providers/testkube/README.md @@ -31,8 +31,8 @@ brew install minikube kubectl minikube start --driver=docker --profile testkube kubectl config use-context testkube -docker build -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest ./v1/providers/testkube/images/ubuntu-vm -minikube --profile testkube image load ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest +docker build -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2 ./v1/providers/testkube/images/ubuntu-vm +minikube --profile testkube image load ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2 kubectl create namespace testkube # In another terminal, keep this running while validation runs. @@ -52,4 +52,4 @@ Clean up: ```bash minikube --profile testkube delete -``` \ No newline at end of file +``` diff --git a/v1/providers/testkube/images/ubuntu-vm/README.md b/v1/providers/testkube/images/ubuntu-vm/README.md index 5a84978..97ef6e1 100644 --- a/v1/providers/testkube/images/ubuntu-vm/README.md +++ b/v1/providers/testkube/images/ubuntu-vm/README.md @@ -1,9 +1,10 @@ # Testkube Ubuntu VM Image -This image backs the `test.ok.cpu` testkube instance type: +This image backs the architecture-specific TestKube instance types: ```text -ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest +test.ok.cpu -> ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2 +test.ok.cpu.arm64 -> ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2 ``` ## Publish to GHCR @@ -16,22 +17,12 @@ gh auth refresh -h github.com -s write:packages gh auth token | docker login ghcr.io -u "$(gh api user --jq .login)" --password-stdin ``` -Build and push the image from the repository root. For EKS, publish an amd64 image because `test.ok.cpu` advertises `x86_64`: - -```bash -docker buildx build \ - --platform linux/amd64 \ - -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest \ - --push \ - ./v1/providers/testkube/images/ubuntu-vm -``` - -If you need both local Apple Silicon clusters and amd64 EKS nodes to pull the same tag, publish a multi-arch manifest: +Build and push the image from the repository root. For EKS, publish the versioned multi-arch manifest used by both `test.ok.cpu` and `test.ok.cpu.arm64`: ```bash docker buildx build \ --platform linux/amd64,linux/arm64 \ - -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest \ + -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2 \ --push \ ./v1/providers/testkube/images/ubuntu-vm ``` @@ -50,6 +41,6 @@ For local minikube or kind validation where the image is loaded directly into th ```bash docker build \ - -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest \ + -t ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2 \ ./v1/providers/testkube/images/ubuntu-vm ``` diff --git a/v1/providers/testkube/instance.go b/v1/providers/testkube/instance.go index 9d981bb..fd4c754 100644 --- a/v1/providers/testkube/instance.go +++ b/v1/providers/testkube/instance.go @@ -133,6 +133,7 @@ func (c *TestKubeClient) createInstanceAsK8sResources(ctx context.Context, attrs Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, TerminationGracePeriodSeconds: int64Ptr(1), + NodeSelector: maps.Clone(instanceTypeSpec.nodeSelector), Containers: []corev1.Container{ { Name: "vm", diff --git a/v1/providers/testkube/instance_test.go b/v1/providers/testkube/instance_test.go index 10aab7c..906e87c 100644 --- a/v1/providers/testkube/instance_test.go +++ b/v1/providers/testkube/instance_test.go @@ -176,6 +176,66 @@ func TestInstanceUsesBakedImageSpec(t *testing.T) { } } +func TestARM64InstanceUsesVersionedMultiarchImage(t *testing.T) { + ctx := context.Background() + client := newTestClient(t) + + instance, err := client.CreateInstance(ctx, cloudv1.CreateInstanceAttrs{ + RefID: "arm64-image-spec", + Name: "arm64 image spec", + InstanceType: InstanceTypeOKCPUARM64, + }) + require.NoError(t, err) + require.Equal(t, "testkube-ubuntu-vm-arm64", instance.ImageID) + + pod, err := client.k8sClient.CoreV1().Pods(client.namespace).Get(ctx, string(instance.CloudID), metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2", pod.Spec.Containers[0].Image) +} + +func TestInstanceUsesArchitectureNodeSelector(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + instanceType string + nodeArchitecture string + }{ + { + name: "x86_64", + instanceType: InstanceTypeOKCPU, + nodeArchitecture: "amd64", + }, + { + name: "arm64", + instanceType: InstanceTypeOKCPUARM64, + nodeArchitecture: "arm64", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := newTestClient(t) + instance, err := client.CreateInstance(ctx, cloudv1.CreateInstanceAttrs{ + RefID: "node-selector-" + tt.name, + Name: "node selector " + tt.name, + InstanceType: tt.instanceType, + }) + require.NoError(t, err) + + pod, err := client.k8sClient.CoreV1().Pods(client.namespace).Get( + ctx, + string(instance.CloudID), + metav1.GetOptions{}, + ) + require.NoError(t, err) + require.Equal(t, map[string]string{ + corev1.LabelArchStable: tt.nodeArchitecture, + }, pod.Spec.NodeSelector) + }) + } +} + func TestPopulateNetworkLoadBalancer(t *testing.T) { instance := &cloudv1.Instance{} populateNetwork(&corev1.Service{ diff --git a/v1/providers/testkube/instancetype.go b/v1/providers/testkube/instancetype.go index 152f35e..73b9c3a 100644 --- a/v1/providers/testkube/instancetype.go +++ b/v1/providers/testkube/instancetype.go @@ -13,11 +13,13 @@ import ( const ( DefaultImageID = "testkube-ubuntu-vm" - DefaultImage = "ghcr.io/brevdev/cloud/testkube-ubuntu-vm:latest" + DefaultImage = "ghcr.io/brevdev/cloud/testkube-ubuntu-vm:multiarch-v2" + ARM64ImageID = "testkube-ubuntu-vm-arm64" DefaultPriceCentsPerHour = 1 InstanceTypeOKCPU = "test.ok.cpu" + InstanceTypeOKCPUARM64 = "test.ok.cpu.arm64" InstanceTypeFailCapacity = "test.fail.capacity" InstanceTypeFailQuota = "test.fail.quota" InstanceTypeFailBuild = "test.fail.build" // TODO: trigger build failure, maybe with a process that monitors build? @@ -29,23 +31,36 @@ type instanceTypeSpec struct { instanceType cloudv1.InstanceType imageID string image string + nodeSelector map[string]string serviceType corev1.ServiceType } var allInstanceTypeSpecs = []instanceTypeSpec{ - makeInstanceTypeSpec(InstanceTypeOKCPU), - makeInstanceTypeSpec(InstanceTypeFailCapacity), - makeInstanceTypeSpec(InstanceTypeFailQuota), - makeInstanceTypeSpec(InstanceTypeFailBuild), + makeInstanceTypeSpec(InstanceTypeOKCPU, cloudv1.ArchitectureX86_64, DefaultImageID), + makeInstanceTypeSpec(InstanceTypeOKCPUARM64, cloudv1.ArchitectureARM64, ARM64ImageID), + makeInstanceTypeSpec(InstanceTypeFailCapacity, cloudv1.ArchitectureX86_64, DefaultImageID), + makeInstanceTypeSpec(InstanceTypeFailQuota, cloudv1.ArchitectureX86_64, DefaultImageID), + makeInstanceTypeSpec(InstanceTypeFailBuild, cloudv1.ArchitectureX86_64, DefaultImageID), } -func makeInstanceTypeSpec(instanceType string) instanceTypeSpec { +func makeInstanceTypeSpec( + instanceType string, + architecture cloudv1.Architecture, + imageID string, +) instanceTypeSpec { estimatedDeployTime := 20 * time.Second + nodeArchitecture := string(architecture) + if architecture == cloudv1.ArchitectureX86_64 { + nodeArchitecture = "amd64" + } return instanceTypeSpec{ - instanceType: makeCPUInstanceType(instanceType, true, &estimatedDeployTime), - imageID: DefaultImageID, + instanceType: makeCPUInstanceType(instanceType, architecture, true, &estimatedDeployTime), + imageID: imageID, image: DefaultImage, - serviceType: corev1.ServiceTypeLoadBalancer, + nodeSelector: map[string]string{ + corev1.LabelArchStable: nodeArchitecture, + }, + serviceType: corev1.ServiceTypeLoadBalancer, } } @@ -118,7 +133,12 @@ func (c *TestKubeClient) instanceTypeSpecToBrevInstanceType(spec instanceTypeSpe return instanceType } -func makeCPUInstanceType(instanceType string, available bool, estimatedDeployTime *time.Duration) cloudv1.InstanceType { +func makeCPUInstanceType( + instanceType string, + architecture cloudv1.Architecture, + available bool, + estimatedDeployTime *time.Duration, +) cloudv1.InstanceType { basePrice, _ := currency.NewAmountFromInt64(DefaultPriceCentsPerHour, "USD") it := cloudv1.InstanceType{ Type: instanceType, @@ -139,7 +159,7 @@ func makeCPUInstanceType(instanceType string, available bool, estimatedDeployTim DefaultCores: 2, VCPU: 2, SupportedArchitectures: []cloudv1.Architecture{ - cloudv1.ArchitectureX86_64, + architecture, }, Stoppable: false, Rebootable: false, diff --git a/v1/providers/testkube/instancetype_test.go b/v1/providers/testkube/instancetype_test.go index 0762a81..5a5a616 100644 --- a/v1/providers/testkube/instancetype_test.go +++ b/v1/providers/testkube/instancetype_test.go @@ -14,7 +14,7 @@ func TestGetInstanceTypes(t *testing.T) { instanceTypes, err := client.GetInstanceTypes(context.Background(), cloudv1.GetInstanceTypeArgs{}) require.NoError(t, err) - require.Len(t, instanceTypes, 4) + require.Len(t, instanceTypes, 5) instanceTypeByName := map[string]cloudv1.InstanceType{} for _, instanceType := range instanceTypes { @@ -26,6 +26,7 @@ func TestGetInstanceTypes(t *testing.T) { InstanceTypeFailCapacity, InstanceTypeFailQuota, InstanceTypeFailBuild, + InstanceTypeOKCPUARM64, } { instanceType, ok := instanceTypeByName[expected] require.True(t, ok, "missing instance type %s", expected) @@ -40,6 +41,49 @@ func TestGetInstanceTypes(t *testing.T) { } } +func TestGetInstanceTypesFiltersByArchitecture(t *testing.T) { + client := newTestClient(t) + + tests := []struct { + name string + architecture cloudv1.Architecture + expected []string + }{ + { + name: "x86_64", + architecture: cloudv1.ArchitectureX86_64, + expected: []string{ + InstanceTypeOKCPU, + InstanceTypeFailCapacity, + InstanceTypeFailQuota, + InstanceTypeFailBuild, + }, + }, + { + name: "arm64", + architecture: cloudv1.ArchitectureARM64, + expected: []string{InstanceTypeOKCPUARM64}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + instanceTypes, err := client.GetInstanceTypes(context.Background(), cloudv1.GetInstanceTypeArgs{ + ArchitectureFilter: &cloudv1.ArchitectureFilter{ + IncludeArchitectures: []cloudv1.Architecture{tt.architecture}, + }, + }) + require.NoError(t, err) + + actual := make([]string, 0, len(instanceTypes)) + for _, instanceType := range instanceTypes { + actual = append(actual, instanceType.Type) + } + require.ElementsMatch(t, tt.expected, actual) + }) + } +} + func TestGetInstanceTypesWithGPUManufacturerFilterIncludesCPU(t *testing.T) { client := newTestClient(t) @@ -49,7 +93,7 @@ func TestGetInstanceTypesWithGPUManufacturerFilterIncludesCPU(t *testing.T) { }, }) require.NoError(t, err) - require.Len(t, instanceTypes, 4) + require.Len(t, instanceTypes, 5) } func TestCapabilitiesDoNotAdvertiseImages(t *testing.T) { From 77203eecd07f2561a51cad06cd48c0da8dc3b43d Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Wed, 29 Jul 2026 10:41:44 -0700 Subject: [PATCH 16/17] gb300 --- v1/providers/nebius/instancetype.go | 1 + v1/providers/nebius/instancetype_test.go | 1 + v1/providers/nebius/integration_test.go | 34 +++++++++++++++++------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index 8890321..13ef3d1 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -441,6 +441,7 @@ func (c *NebiusClient) getGPUQuotaName(platformName string) string { } var nebiusPlatformArchitectures = map[string]v1.Architecture{ + "gpu-gb300": v1.ArchitectureARM64, "gpu-b300-sxm": v1.ArchitectureX86_64, "gpu-b200-sxm": v1.ArchitectureX86_64, "gpu-b200-sxm-a": v1.ArchitectureX86_64, diff --git a/v1/providers/nebius/instancetype_test.go b/v1/providers/nebius/instancetype_test.go index 87f1ffe..248f7e9 100644 --- a/v1/providers/nebius/instancetype_test.go +++ b/v1/providers/nebius/instancetype_test.go @@ -13,6 +13,7 @@ func TestNebiusPlatformArchitecture(t *testing.T) { require.Equal(t, cloudv1.ArchitectureX86_64, nebiusPlatformArchitecture(" GPU-H100-SXM ")) require.Equal(t, cloudv1.ArchitectureX86_64, nebiusPlatformArchitecture("cpu-d3")) + require.Equal(t, cloudv1.ArchitectureARM64, nebiusPlatformArchitecture("gpu-gb300")) require.Equal(t, cloudv1.ArchitectureUnknown, nebiusPlatformArchitecture("future-platform")) } diff --git a/v1/providers/nebius/integration_test.go b/v1/providers/nebius/integration_test.go index 372dbe4..ae2dec5 100644 --- a/v1/providers/nebius/integration_test.go +++ b/v1/providers/nebius/integration_test.go @@ -234,15 +234,29 @@ func TestIntegration_InstanceLifecycle(t *testing.T) { // Step 0: Get available instance types to find one we can use t.Log("Discovering available instance types...") - instanceTypes, err := client.GetInstanceTypes(ctx, v1.GetInstanceTypeArgs{}) + instanceTypes, err := client.GetInstanceTypes(ctx, v1.GetInstanceTypeArgs{ + ArchitectureFilter: &v1.ArchitectureFilter{ + IncludeArchitectures: []v1.Architecture{v1.ArchitectureX86_64}, + }, + }) require.NoError(t, err, "Failed to get instance types") if len(instanceTypes) == 0 { - t.Skip("No instance types available - skipping instance lifecycle test") + t.Skip("No x86_64 instance types available - skipping instance lifecycle test") + } + + selectedInstanceTypeIndex := -1 + for i := range instanceTypes { + if instanceTypes[i].IsAvailable { + selectedInstanceTypeIndex = i + break + } + } + if selectedInstanceTypeIndex == -1 { + t.Skip("No available x86_64 instance types - skipping instance lifecycle test") } - // Use the first available instance type (should have quota) - selectedInstanceType := instanceTypes[0] + selectedInstanceType := instanceTypes[selectedInstanceTypeIndex] t.Logf("Using instance type: %s (Location: %s)", selectedInstanceType.ID, selectedInstanceType.Location) // Step 0.5: Generate SSH key pair for testing (inspired by Shadeform's SSH key handling) @@ -256,11 +270,11 @@ func TestIntegration_InstanceLifecycle(t *testing.T) { createAttrs := v1.CreateInstanceAttrs{ RefID: instanceRefID, Name: instanceName, - InstanceType: string(selectedInstanceType.ID), // Use discovered instance type - ImageID: "ubuntu22.04-cuda12", // Use known-good Nebius image family - DiskSize: 50 * 1024 * 1024 * 1024, // 50 GiB in bytes - Location: selectedInstanceType.Location, // Use the instance type's location - PublicKey: publicKey, // SSH public key for access (like Shadeform) + InstanceType: selectedInstanceType.Type, // Native {platform}.{preset} format + ImageID: "ubuntu22.04-cuda12", // Use known-good Nebius image family + DiskSize: 50 * 1024 * 1024 * 1024, // 50 GiB in bytes + Location: selectedInstanceType.Location, // Use the instance type's location + PublicKey: publicKey, // SSH public key for access (like Shadeform) Tags: map[string]string{ "test": "integration", "created-by": "nebius-integration-test", @@ -565,7 +579,7 @@ func TestIntegration_GetInstanceTypes(t *testing.T) { priceStr := it.BasePrice.Number() var priceFloat float64 if _, err := fmt.Sscanf(priceStr, "%f", &priceFloat); err == nil { - assert.Greater(t, priceFloat, 0.0, "Price should be positive") + assert.GreaterOrEqual(t, priceFloat, 0.0, "Price should not be negative") assert.Less(t, priceFloat, 1000.0, "Price per hour should be reasonable (< $1000/hr)") } } else { From c249b7911d4ac2f02998c41bed77036bc70d6f7c Mon Sep 17 00:00:00 2001 From: Pratik Patel Date: Thu, 30 Jul 2026 09:20:54 -0700 Subject: [PATCH 17/17] fix gb prefix and add gb200s --- v1/providers/nebius/instance_test.go | 20 ++++++++++ v1/providers/nebius/instancetype.go | 47 +++++++++++++++++++----- v1/providers/nebius/instancetype_test.go | 1 + 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/v1/providers/nebius/instance_test.go b/v1/providers/nebius/instance_test.go index ee1c8f7..3a5523e 100644 --- a/v1/providers/nebius/instance_test.go +++ b/v1/providers/nebius/instance_test.go @@ -212,10 +212,12 @@ func TestGetGPUMemory(t *testing.T) { gpuType: "B200", expectedGiB: 192, }, + {gpuType: "GB200", expectedGiB: 192}, { gpuType: "B300", expectedGiB: 270, }, + {gpuType: "GB300", expectedGiB: 270}, { gpuType: "RTX6000", expectedGiB: 96, @@ -285,11 +287,13 @@ func TestExtractGPUTypeAndName(t *testing.T) { expectedType: "B200", expectedName: "B200", }, + {platformName: "gpu-gb200", expectedType: "GB200", expectedName: "GB200"}, { platformName: "gpu-b300-sxm", expectedType: "B300", expectedName: "B300", }, + {platformName: "gpu-gb300", expectedType: "GB300", expectedName: "GB300"}, { platformName: "gpu-rtx6000", expectedType: "RTX6000", @@ -318,6 +322,22 @@ func TestExtractGPUTypeAndName(t *testing.T) { } } +func TestGetGPUQuotaName(t *testing.T) { + client := &NebiusClient{} + tests := map[string]string{ + "gpu-gb200": "compute.instance.gpu.gb200", + "gpu-gb300": "compute.instance.gpu.gb300", + "gpu-b300-sxm": "compute.instance.gpu.b300", + "gpu-rtx6000": "compute.instance.gpu.rtx6000", + } + + for platformName, expected := range tests { + t.Run(platformName, func(t *testing.T) { + assert.Equal(t, expected, client.getGPUQuotaName(platformName)) + }) + } +} + func TestGetNebiusBootImageFamily(t *testing.T) { tests := []struct { name string diff --git a/v1/providers/nebius/instancetype.go b/v1/providers/nebius/instancetype.go index 13ef3d1..07e9a48 100644 --- a/v1/providers/nebius/instancetype.go +++ b/v1/providers/nebius/instancetype.go @@ -416,8 +416,11 @@ func (c *NebiusClient) getGPUQuotaName(platformName string) string { // Nebius GPU quota names follow pattern: "compute.instance.gpu.{type}" // Examples: "compute.instance.gpu.h100", "compute.instance.gpu.h200", "compute.instance.gpu.l40s" - platformLower := strings.ToLower(platformName) + if gpuType := parseBlackwellGPUType(platformName); gpuType != "" { + return "compute.instance.gpu." + strings.ToLower(gpuType) + } + platformLower := strings.ToLower(platformName) if strings.Contains(platformLower, "h100") { return "compute.instance.gpu.h100" } @@ -433,14 +436,15 @@ func (c *NebiusClient) getGPUQuotaName(platformName string) string { if strings.Contains(platformLower, "v100") { return "compute.instance.gpu.v100" } - if strings.Contains(platformLower, "b200") { - return "compute.instance.gpu.b200" + if strings.Contains(platformLower, "rtx6000") { + return "compute.instance.gpu.rtx6000" } return "" } var nebiusPlatformArchitectures = map[string]v1.Architecture{ + "gpu-gb200": v1.ArchitectureARM64, "gpu-gb300": v1.ArchitectureARM64, "gpu-b300-sxm": v1.ArchitectureX86_64, "gpu-b200-sxm": v1.ArchitectureX86_64, @@ -466,10 +470,14 @@ func nebiusPlatformArchitecture(platformName string) v1.Architecture { func (c *NebiusClient) isPlatformSupported(platformName string) bool { platformLower := strings.ToLower(platformName) + if parseBlackwellGPUType(platformName) != "" { + return true + } + // For GPU platforms: only accept known GPU types // Check for specific GPU model names (with or without "gpu-" prefix) knownGPUTypes := []string{ - "h100", "h200", "l40s", "a100", "v100", "a10", "t4", "l4", "b200", "b300", "rtx6000", + "h100", "h200", "l40s", "a100", "v100", "a10", "t4", "l4", "rtx6000", } for _, gpuType := range knownGPUTypes { if strings.Contains(platformLower, gpuType) { @@ -560,6 +568,10 @@ func supportsAllowedArchitecture(instanceType v1.InstanceType, filter *v1.Archit // Note: Returns model name only (e.g., "H100"), not full name with manufacturer // Manufacturer info is stored separately in GPU.Manufacturer field func extractGPUTypeAndName(platformName string) (string, string) { + if gpuType := parseBlackwellGPUType(platformName); gpuType != "" { + return gpuType, gpuType + } + platformLower := strings.ToLower(platformName) if strings.Contains(platformLower, "h100") { @@ -577,12 +589,6 @@ func extractGPUTypeAndName(platformName string) (string, string) { if strings.Contains(platformLower, "v100") { return "V100", "V100" } - if strings.Contains(platformLower, "b200") { - return "B200", "B200" - } - if strings.Contains(platformLower, "b300") { - return "B300", "B300" - } if strings.Contains(platformLower, "rtx6000") { return "RTX6000", "RTX6000" } @@ -590,6 +596,25 @@ func extractGPUTypeAndName(platformName string) (string, string) { return "GPU", "GPU" // Generic fallback } +func parseBlackwellGPUType(platformName string) string { + platformLower := strings.ToLower(platformName) + + // Keep Grace Blackwell variants before their matching B-series variants: + // "gb300" contains "b300", and "gb200" contains "b200". + switch { + case strings.Contains(platformLower, "gb300"): + return "GB300" + case strings.Contains(platformLower, "gb200"): + return "GB200" + case strings.Contains(platformLower, "b300"): + return "B300" + case strings.Contains(platformLower, "b200"): + return "B200" + default: + return "" + } +} + // getGPUMemory returns the VRAM for a given GPU type in GiB func getGPUMemory(gpuType string) units.Base2Bytes { // Static mapping of GPU types to their VRAM capacities @@ -603,7 +628,9 @@ func getGPUMemory(gpuType string) units.Base2Bytes { "T4": 16, // 16 GiB VRAM "L4": 24, // 24 GiB VRAM "B200": 192, // 192 GiB VRAM + "GB200": 192, // 192 GiB VRAM "B300": 270, // 270 GiB VRAM + "GB300": 270, // 270 GiB VRAM "RTX6000": 96, // 96 GiB VRAM } diff --git a/v1/providers/nebius/instancetype_test.go b/v1/providers/nebius/instancetype_test.go index 248f7e9..4a7915f 100644 --- a/v1/providers/nebius/instancetype_test.go +++ b/v1/providers/nebius/instancetype_test.go @@ -13,6 +13,7 @@ func TestNebiusPlatformArchitecture(t *testing.T) { require.Equal(t, cloudv1.ArchitectureX86_64, nebiusPlatformArchitecture(" GPU-H100-SXM ")) require.Equal(t, cloudv1.ArchitectureX86_64, nebiusPlatformArchitecture("cpu-d3")) + require.Equal(t, cloudv1.ArchitectureARM64, nebiusPlatformArchitecture("gpu-gb200")) require.Equal(t, cloudv1.ArchitectureARM64, nebiusPlatformArchitecture("gpu-gb300")) require.Equal(t, cloudv1.ArchitectureUnknown, nebiusPlatformArchitecture("future-platform")) }