diff --git a/.stats.yml b/.stats.yml
index d6e9571..55ad565 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 30
+configured_endpoints: 36
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-e052ac01c788e7e3e46c96bf3c42be7ae57f9dd046129add8012d0eeb388e884.yml
openapi_spec_hash: fd805921c0162d63405f5feb7e8c7082
-config_hash: 170041ea532e81620e0f6a73f6b31d44
+config_hash: 14135b7c88169a15762c8defb0bdfd16
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58bf980..4405152 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,12 +2,12 @@
## 0.9.0 (2026-01-05)
-Full Changelog: [v0.8.0...v0.9.0](https://github.com/onkernel/hypeman-go/compare/v0.8.0...v0.9.0)
+Full Changelog: [v0.8.0...v0.9.0](https://github.com/kernel/hypeman-go/compare/v0.8.0...v0.9.0)
### Features
-* QEMU support ([d708091](https://github.com/onkernel/hypeman-go/commit/d70809169d136df3f1efbf961f2a90084e1f9fa5))
-* Resource accounting ([4141287](https://github.com/onkernel/hypeman-go/commit/414128770e8137ed2a40d404f0f4ac06ea1a0731))
+* QEMU support ([d708091](https://github.com/kernel/hypeman-go/commit/d70809169d136df3f1efbf961f2a90084e1f9fa5))
+* Resource accounting ([4141287](https://github.com/kernel/hypeman-go/commit/414128770e8137ed2a40d404f0f4ac06ea1a0731))
## 0.8.0 (2025-12-23)
diff --git a/README.md b/README.md
index 20c5d75..d20968d 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Or to pin the version:
```sh
-go get -u 'github.com/onkernel/hypeman-go@v0.9.0'
+go get -u 'github.com/kernel/hypeman-go@v0.9.0'
```
@@ -342,6 +342,24 @@ file returned by `os.Open` will be sent with the file name on disk.
We also provide a helper `hypeman.File(reader io.Reader, filename string, contentType string)`
which can be used to wrap any `io.Reader` with the appropriate file name and content type.
+```go
+// A file from the file system
+file, err := os.Open("/path/to/file")
+hypeman.BuildNewParams{
+ Source: file,
+}
+
+// A file from a string
+hypeman.BuildNewParams{
+ Source: strings.NewReader("my file contents"),
+}
+
+// With a custom filename and contentType
+hypeman.BuildNewParams{
+ Source: hypeman.File(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"),
+}
+```
+
### Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
diff --git a/api.md b/api.md
index 4b1b164..ae9d5bb 100644
--- a/api.md
+++ b/api.md
@@ -104,3 +104,35 @@ Methods:
- client.Ingresses.List(ctx context.Context) ([]hypeman.Ingress, error)
- client.Ingresses.Delete(ctx context.Context, id string) error
- client.Ingresses.Get(ctx context.Context, id string) (hypeman.Ingress, error)
+# Resources
+
+Response Types:
+
+- hypeman.DiskBreakdown
+- hypeman.GPUProfile
+- hypeman.GPUResourceStatus
+- hypeman.PassthroughDevice
+- hypeman.ResourceAllocation
+- hypeman.ResourceStatus
+- hypeman.Resources
+
+Methods:
+
+- client.Resources.Get(ctx context.Context) (\*hypeman.Resources, error)
+
+# Builds
+
+Response Types:
+
+- hypeman.Build
+- hypeman.BuildEvent
+- hypeman.BuildProvenance
+- hypeman.BuildStatus
+
+Methods:
+
+- client.Builds.New(ctx context.Context, body hypeman.BuildNewParams) (\*hypeman.Build, error)
+- client.Builds.List(ctx context.Context) (\*[]hypeman.Build, error)
+- client.Builds.Cancel(ctx context.Context, id string) error
+- client.Builds.Events(ctx context.Context, id string, query hypeman.BuildEventsParams) (\*hypeman.BuildEvent, error)
+- client.Builds.Get(ctx context.Context, id string) (\*hypeman.Build, error)
diff --git a/build.go b/build.go
new file mode 100644
index 0000000..b10833e
--- /dev/null
+++ b/build.go
@@ -0,0 +1,287 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "slices"
+ "time"
+
+ "github.com/kernel/hypeman-go/internal/apiform"
+ "github.com/kernel/hypeman-go/internal/apijson"
+ "github.com/kernel/hypeman-go/internal/apiquery"
+ "github.com/kernel/hypeman-go/internal/requestconfig"
+ "github.com/kernel/hypeman-go/option"
+ "github.com/kernel/hypeman-go/packages/param"
+ "github.com/kernel/hypeman-go/packages/respjson"
+ "github.com/kernel/hypeman-go/packages/ssestream"
+)
+
+// BuildService contains methods and other services that help with interacting with
+// the hypeman API.
+//
+// Note, unlike clients, this service does not read variables from the environment
+// automatically. You should not instantiate this service directly, and instead use
+// the [NewBuildService] method instead.
+type BuildService struct {
+ Options []option.RequestOption
+}
+
+// NewBuildService generates a new service that applies the given options to each
+// request. These options are applied after the parent client's options (if there
+// is one), and before any request-specific options.
+func NewBuildService(opts ...option.RequestOption) (r BuildService) {
+ r = BuildService{}
+ r.Options = opts
+ return
+}
+
+// Creates a new build job. Source code should be uploaded as a tar.gz archive in
+// the multipart form data.
+func (r *BuildService) New(ctx context.Context, body BuildNewParams, opts ...option.RequestOption) (res *Build, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "builds"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
+// List builds
+func (r *BuildService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Build, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "builds"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// Cancel build
+func (r *BuildService) Cancel(ctx context.Context, id string, opts ...option.RequestOption) (err error) {
+ opts = slices.Concat(r.Options, opts)
+ opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("builds/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...)
+ return
+}
+
+// Streams build events as Server-Sent Events. Events include:
+//
+// - `log`: Build log lines with timestamp and content
+// - `status`: Build status changes (queued→building→pushing→ready/failed)
+// - `heartbeat`: Keep-alive events sent every 30s to prevent connection timeouts
+//
+// Returns existing logs as events, then continues streaming if follow=true.
+func (r *BuildService) EventsStreaming(ctx context.Context, id string, query BuildEventsParams, opts ...option.RequestOption) (stream *ssestream.Stream[BuildEvent]) {
+ var (
+ raw *http.Response
+ err error
+ )
+ opts = slices.Concat(r.Options, opts)
+ opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("builds/%s/events", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...)
+ return ssestream.NewStream[BuildEvent](ssestream.NewDecoder(raw), err)
+}
+
+// Get build details
+func (r *BuildService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Build, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("builds/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+type Build struct {
+ // Build job identifier
+ ID string `json:"id,required"`
+ // Build creation timestamp
+ CreatedAt time.Time `json:"created_at,required" format:"date-time"`
+ // Build job status
+ //
+ // Any of "queued", "building", "pushing", "ready", "failed", "cancelled".
+ Status BuildStatus `json:"status,required"`
+ // Build completion timestamp
+ CompletedAt time.Time `json:"completed_at,nullable" format:"date-time"`
+ // Build duration in milliseconds
+ DurationMs int64 `json:"duration_ms,nullable"`
+ // Error message (only when status is failed)
+ Error string `json:"error,nullable"`
+ // Digest of built image (only when status is ready)
+ ImageDigest string `json:"image_digest,nullable"`
+ // Full image reference (only when status is ready)
+ ImageRef string `json:"image_ref,nullable"`
+ Provenance BuildProvenance `json:"provenance"`
+ // Position in build queue (only when status is queued)
+ QueuePosition int64 `json:"queue_position,nullable"`
+ // Build start timestamp
+ StartedAt time.Time `json:"started_at,nullable" format:"date-time"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ ID respjson.Field
+ CreatedAt respjson.Field
+ Status respjson.Field
+ CompletedAt respjson.Field
+ DurationMs respjson.Field
+ Error respjson.Field
+ ImageDigest respjson.Field
+ ImageRef respjson.Field
+ Provenance respjson.Field
+ QueuePosition respjson.Field
+ StartedAt respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r Build) RawJSON() string { return r.JSON.raw }
+func (r *Build) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type BuildEvent struct {
+ // Event timestamp
+ Timestamp time.Time `json:"timestamp,required" format:"date-time"`
+ // Event type
+ //
+ // Any of "log", "status", "heartbeat".
+ Type BuildEventType `json:"type,required"`
+ // Log line content (only for type=log)
+ Content string `json:"content"`
+ // New build status (only for type=status)
+ //
+ // Any of "queued", "building", "pushing", "ready", "failed", "cancelled".
+ Status BuildStatus `json:"status"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Timestamp respjson.Field
+ Type respjson.Field
+ Content respjson.Field
+ Status respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r BuildEvent) RawJSON() string { return r.JSON.raw }
+func (r *BuildEvent) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// Event type
+type BuildEventType string
+
+const (
+ BuildEventTypeLog BuildEventType = "log"
+ BuildEventTypeStatus BuildEventType = "status"
+ BuildEventTypeHeartbeat BuildEventType = "heartbeat"
+)
+
+type BuildProvenance struct {
+ // Pinned base image digest used
+ BaseImageDigest string `json:"base_image_digest"`
+ // BuildKit version used
+ BuildkitVersion string `json:"buildkit_version"`
+ // Map of lockfile names to SHA256 hashes
+ LockfileHashes map[string]string `json:"lockfile_hashes"`
+ // SHA256 hash of source tarball
+ SourceHash string `json:"source_hash"`
+ // Build completion timestamp
+ Timestamp time.Time `json:"timestamp" format:"date-time"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ BaseImageDigest respjson.Field
+ BuildkitVersion respjson.Field
+ LockfileHashes respjson.Field
+ SourceHash respjson.Field
+ Timestamp respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r BuildProvenance) RawJSON() string { return r.JSON.raw }
+func (r *BuildProvenance) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// Build job status
+type BuildStatus string
+
+const (
+ BuildStatusQueued BuildStatus = "queued"
+ BuildStatusBuilding BuildStatus = "building"
+ BuildStatusPushing BuildStatus = "pushing"
+ BuildStatusReady BuildStatus = "ready"
+ BuildStatusFailed BuildStatus = "failed"
+ BuildStatusCancelled BuildStatus = "cancelled"
+)
+
+type BuildNewParams struct {
+ // Source tarball (tar.gz) containing application code and optionally a Dockerfile
+ Source io.Reader `json:"source,omitzero,required" format:"binary"`
+ // Optional pinned base image digest
+ BaseImageDigest param.Opt[string] `json:"base_image_digest,omitzero"`
+ // Tenant-specific cache key prefix
+ CacheScope param.Opt[string] `json:"cache_scope,omitzero"`
+ // Dockerfile content. Required if not included in the source tarball.
+ Dockerfile param.Opt[string] `json:"dockerfile,omitzero"`
+ // JSON array of secret references to inject during build. Each object has "id"
+ // (required) for use with --mount=type=secret,id=... Example: [{"id":
+ // "npm_token"}, {"id": "github_token"}]
+ Secrets param.Opt[string] `json:"secrets,omitzero"`
+ // Build timeout (default 600)
+ TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"`
+ paramObj
+}
+
+func (r BuildNewParams) MarshalMultipart() (data []byte, contentType string, err error) {
+ buf := bytes.NewBuffer(nil)
+ writer := multipart.NewWriter(buf)
+ err = apiform.MarshalRoot(r, writer)
+ if err == nil {
+ err = apiform.WriteExtras(writer, r.ExtraFields())
+ }
+ if err != nil {
+ writer.Close()
+ return nil, "", err
+ }
+ err = writer.Close()
+ if err != nil {
+ return nil, "", err
+ }
+ return buf.Bytes(), writer.FormDataContentType(), nil
+}
+
+type BuildEventsParams struct {
+ // Continue streaming new events after initial output
+ Follow param.Opt[bool] `query:"follow,omitzero" json:"-"`
+ paramObj
+}
+
+// URLQuery serializes [BuildEventsParams]'s query parameters as `url.Values`.
+func (r BuildEventsParams) URLQuery() (v url.Values, err error) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
diff --git a/build_test.go b/build_test.go
new file mode 100644
index 0000000..a76dbb6
--- /dev/null
+++ b/build_test.go
@@ -0,0 +1,115 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman_test
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/kernel/hypeman-go"
+ "github.com/kernel/hypeman-go/internal/testutil"
+ "github.com/kernel/hypeman-go/option"
+)
+
+func TestBuildNewWithOptionalParams(t *testing.T) {
+ t.Skip("Prism tests are disabled")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := hypeman.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithAPIKey("My API Key"),
+ )
+ _, err := client.Builds.New(context.TODO(), hypeman.BuildNewParams{
+ Source: io.Reader(bytes.NewBuffer([]byte("some file contents"))),
+ BaseImageDigest: hypeman.String("base_image_digest"),
+ CacheScope: hypeman.String("cache_scope"),
+ Dockerfile: hypeman.String("dockerfile"),
+ Secrets: hypeman.String("secrets"),
+ TimeoutSeconds: hypeman.Int(0),
+ })
+ if err != nil {
+ var apierr *hypeman.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
+func TestBuildList(t *testing.T) {
+ t.Skip("Prism tests are disabled")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := hypeman.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithAPIKey("My API Key"),
+ )
+ _, err := client.Builds.List(context.TODO())
+ if err != nil {
+ var apierr *hypeman.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
+func TestBuildCancel(t *testing.T) {
+ t.Skip("Prism tests are disabled")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := hypeman.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithAPIKey("My API Key"),
+ )
+ err := client.Builds.Cancel(context.TODO(), "id")
+ if err != nil {
+ var apierr *hypeman.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
+func TestBuildGet(t *testing.T) {
+ t.Skip("Prism tests are disabled")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := hypeman.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithAPIKey("My API Key"),
+ )
+ _, err := client.Builds.Get(context.TODO(), "id")
+ if err != nil {
+ var apierr *hypeman.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
diff --git a/client.go b/client.go
index c583e82..eeef91e 100644
--- a/client.go
+++ b/client.go
@@ -23,6 +23,8 @@ type Client struct {
Volumes VolumeService
Devices DeviceService
Ingresses IngressService
+ Resources ResourceService
+ Builds BuildService
}
// DefaultClientOptions read from the environment (HYPEMAN_API_KEY,
@@ -53,6 +55,8 @@ func NewClient(opts ...option.RequestOption) (r Client) {
r.Volumes = NewVolumeService(opts...)
r.Devices = NewDeviceService(opts...)
r.Ingresses = NewIngressService(opts...)
+ r.Resources = NewResourceService(opts...)
+ r.Builds = NewBuildService(opts...)
return
}
diff --git a/resource.go b/resource.go
new file mode 100644
index 0000000..7806701
--- /dev/null
+++ b/resource.go
@@ -0,0 +1,253 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman
+
+import (
+ "context"
+ "net/http"
+ "slices"
+
+ "github.com/kernel/hypeman-go/internal/apijson"
+ "github.com/kernel/hypeman-go/internal/requestconfig"
+ "github.com/kernel/hypeman-go/option"
+ "github.com/kernel/hypeman-go/packages/respjson"
+)
+
+// ResourceService contains methods and other services that help with interacting
+// with the hypeman API.
+//
+// Note, unlike clients, this service does not read variables from the environment
+// automatically. You should not instantiate this service directly, and instead use
+// the [NewResourceService] method instead.
+type ResourceService struct {
+ Options []option.RequestOption
+}
+
+// NewResourceService generates a new service that applies the given options to
+// each request. These options are applied after the parent client's options (if
+// there is one), and before any request-specific options.
+func NewResourceService(opts ...option.RequestOption) (r ResourceService) {
+ r = ResourceService{}
+ r.Options = opts
+ return
+}
+
+// Returns current host resource capacity, allocation status, and per-instance
+// breakdown. Resources include CPU, memory, disk, and network. Oversubscription
+// ratios are applied to calculate effective limits.
+func (r *ResourceService) Get(ctx context.Context, opts ...option.RequestOption) (res *Resources, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "resources"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+type DiskBreakdown struct {
+ // Disk used by exported rootfs images
+ ImagesBytes int64 `json:"images_bytes"`
+ // Disk used by OCI layer cache (shared blobs)
+ OciCacheBytes int64 `json:"oci_cache_bytes"`
+ // Disk used by instance overlays (rootfs + volume overlays)
+ OverlaysBytes int64 `json:"overlays_bytes"`
+ // Disk used by volumes
+ VolumesBytes int64 `json:"volumes_bytes"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ ImagesBytes respjson.Field
+ OciCacheBytes respjson.Field
+ OverlaysBytes respjson.Field
+ VolumesBytes respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r DiskBreakdown) RawJSON() string { return r.JSON.raw }
+func (r *DiskBreakdown) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// Available vGPU profile
+type GPUProfile struct {
+ // Number of instances that can be created with this profile
+ Available int64 `json:"available,required"`
+ // Frame buffer size in MB
+ FramebufferMB int64 `json:"framebuffer_mb,required"`
+ // Profile name (user-facing)
+ Name string `json:"name,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Available respjson.Field
+ FramebufferMB respjson.Field
+ Name respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r GPUProfile) RawJSON() string { return r.JSON.raw }
+func (r *GPUProfile) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// GPU resource status. Null if no GPUs available.
+type GPUResourceStatus struct {
+ // GPU mode (vgpu for SR-IOV/mdev, passthrough for whole GPU)
+ //
+ // Any of "vgpu", "passthrough".
+ Mode GPUResourceStatusMode `json:"mode,required"`
+ // Total slots (VFs for vGPU, physical GPUs for passthrough)
+ TotalSlots int64 `json:"total_slots,required"`
+ // Slots currently in use
+ UsedSlots int64 `json:"used_slots,required"`
+ // Physical GPUs (only in passthrough mode)
+ Devices []PassthroughDevice `json:"devices"`
+ // Available vGPU profiles (only in vGPU mode)
+ Profiles []GPUProfile `json:"profiles"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Mode respjson.Field
+ TotalSlots respjson.Field
+ UsedSlots respjson.Field
+ Devices respjson.Field
+ Profiles respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r GPUResourceStatus) RawJSON() string { return r.JSON.raw }
+func (r *GPUResourceStatus) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// GPU mode (vgpu for SR-IOV/mdev, passthrough for whole GPU)
+type GPUResourceStatusMode string
+
+const (
+ GPUResourceStatusModeVgpu GPUResourceStatusMode = "vgpu"
+ GPUResourceStatusModePassthrough GPUResourceStatusMode = "passthrough"
+)
+
+// Physical GPU available for passthrough
+type PassthroughDevice struct {
+ // Whether this GPU is available (not attached to an instance)
+ Available bool `json:"available,required"`
+ // GPU name
+ Name string `json:"name,required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Available respjson.Field
+ Name respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r PassthroughDevice) RawJSON() string { return r.JSON.raw }
+func (r *PassthroughDevice) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type ResourceAllocation struct {
+ // vCPUs allocated
+ CPU int64 `json:"cpu"`
+ // Disk allocated in bytes (overlay + volumes)
+ DiskBytes int64 `json:"disk_bytes"`
+ // Instance identifier
+ InstanceID string `json:"instance_id"`
+ // Instance name
+ InstanceName string `json:"instance_name"`
+ // Memory allocated in bytes
+ MemoryBytes int64 `json:"memory_bytes"`
+ // Download bandwidth limit in bytes/sec (external→VM)
+ NetworkDownloadBps int64 `json:"network_download_bps"`
+ // Upload bandwidth limit in bytes/sec (VM→external)
+ NetworkUploadBps int64 `json:"network_upload_bps"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ CPU respjson.Field
+ DiskBytes respjson.Field
+ InstanceID respjson.Field
+ InstanceName respjson.Field
+ MemoryBytes respjson.Field
+ NetworkDownloadBps respjson.Field
+ NetworkUploadBps respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r ResourceAllocation) RawJSON() string { return r.JSON.raw }
+func (r *ResourceAllocation) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type ResourceStatus struct {
+ // Currently allocated resources
+ Allocated int64 `json:"allocated,required"`
+ // Available for allocation (effective_limit - allocated)
+ Available int64 `json:"available,required"`
+ // Raw host capacity
+ Capacity int64 `json:"capacity,required"`
+ // Capacity after oversubscription (capacity \* ratio)
+ EffectiveLimit int64 `json:"effective_limit,required"`
+ // Oversubscription ratio applied
+ OversubRatio float64 `json:"oversub_ratio,required"`
+ // Resource type
+ Type string `json:"type,required"`
+ // How capacity was determined (detected, configured)
+ Source string `json:"source"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Allocated respjson.Field
+ Available respjson.Field
+ Capacity respjson.Field
+ EffectiveLimit respjson.Field
+ OversubRatio respjson.Field
+ Type respjson.Field
+ Source respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r ResourceStatus) RawJSON() string { return r.JSON.raw }
+func (r *ResourceStatus) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type Resources struct {
+ Allocations []ResourceAllocation `json:"allocations,required"`
+ CPU ResourceStatus `json:"cpu,required"`
+ Disk ResourceStatus `json:"disk,required"`
+ Memory ResourceStatus `json:"memory,required"`
+ Network ResourceStatus `json:"network,required"`
+ DiskBreakdown DiskBreakdown `json:"disk_breakdown"`
+ // GPU resource status. Null if no GPUs available.
+ GPU GPUResourceStatus `json:"gpu,nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Allocations respjson.Field
+ CPU respjson.Field
+ Disk respjson.Field
+ Memory respjson.Field
+ Network respjson.Field
+ DiskBreakdown respjson.Field
+ GPU respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r Resources) RawJSON() string { return r.JSON.raw }
+func (r *Resources) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/resource_test.go b/resource_test.go
new file mode 100644
index 0000000..4566d8d
--- /dev/null
+++ b/resource_test.go
@@ -0,0 +1,37 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman_test
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/kernel/hypeman-go"
+ "github.com/kernel/hypeman-go/internal/testutil"
+ "github.com/kernel/hypeman-go/option"
+)
+
+func TestResourceGet(t *testing.T) {
+ t.Skip("Prism tests are disabled")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := hypeman.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithAPIKey("My API Key"),
+ )
+ _, err := client.Resources.Get(context.TODO())
+ if err != nil {
+ var apierr *hypeman.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}