Types for interacting with vast.ai search responses. VastOffer is a data record for a single rentable GPU offer with an EstimateCost helper. VastOffers builds the JSON body for a search request and parses a search response into a list of VastOffer, tolerating missing fields.
using System.Text.Json;
namespace AutoRig.Vast;
/// <summary>One rentable GPU offer from vast.ai's search (api/v0/search/asks/).</summary>
public sealed class VastOffer
{
public required long Id { get; init; }
public required string GpuName { get; init; }
public required int NumGpus { get; init; }
public required float GpuRamMb { get; init; }
public required float DollarsPerHour { get; init; }
public required float Reliability { get; init; }
public required float InetDownMbps { get; init; }
public required float DiskSpaceGb { get; init; }
/// <summary>Worst-case cost for a bounded session, rounded up to the cent.</summary>
public float EstimateCost( int minutes )
=> MathF.Ceiling( DollarsPerHour * minutes / 60f * 100f ) / 100f;
}
/// <summary>
/// Pure request/response shapes for vast.ai offer search — no IO here so the
/// editor client stays a thin transport and everything is unit-testable.
/// </summary>
public static class VastOffers
{
/// <summary>PUT body for api/v0/search/asks/: verified machines with enough
/// GPU RAM for the model, on-demand (ask) pricing, cheapest first. The whole
/// query lives under "q" (the endpoint rejects a bare query, and rejects the
/// old select_cols:"*"), with order/limit inside it.</summary>
public static string BuildSearchBody( int minGpuRamMb, int minDiskGb )
=> JsonSerializer.Serialize( new Dictionary<string, object>
{
["q"] = new Dictionary<string, object>
{
["gpu_ram"] = new Dictionary<string, object> { ["gte"] = minGpuRamMb },
["disk_space"] = new Dictionary<string, object> { ["gte"] = minDiskGb },
["rentable"] = new Dictionary<string, object> { ["eq"] = true },
["verified"] = new Dictionary<string, object> { ["eq"] = true },
["num_gpus"] = new Dictionary<string, object> { ["eq"] = 1 },
["order"] = new object[] { new object[] { "dph_total", "asc" } },
["limit"] = 32,
},
} );
/// <summary>Parses the search response ({"offers":[...]}), tolerating
/// missing fields — vast's schema drifts and a partial row is still useful.</summary>
public static List<VastOffer> ParseOffers( string json )
{
ArgumentNullException.ThrowIfNull( json );
using var doc = JsonDocument.Parse( json );
if ( !doc.RootElement.TryGetProperty( "offers", out var offers )
|| offers.ValueKind != JsonValueKind.Array )
throw new FormatException( "vast.ai search response has no 'offers' array." );
var result = new List<VastOffer>();
foreach ( var offer in offers.EnumerateArray() )
{
if ( !offer.TryGetProperty( "id", out var id ) )
continue;
result.Add( new VastOffer
{
Id = id.GetInt64(),
GpuName = Str( offer, "gpu_name" ) ?? "GPU",
NumGpus = (int)Num( offer, "num_gpus", 1 ),
GpuRamMb = Num( offer, "gpu_ram", 0 ),
DollarsPerHour = Num( offer, "dph_total", 0 ),
Reliability = Num( offer, "reliability2", Num( offer, "reliability", 0 ) ),
InetDownMbps = Num( offer, "inet_down", 0 ),
DiskSpaceGb = Num( offer, "disk_space", 0 ),
} );
}
return result;
}
static string Str( JsonElement e, string name )
=> e.TryGetProperty( name, out var v ) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
static float Num( JsonElement e, string name, float fallback )
=> e.TryGetProperty( name, out var v ) && v.ValueKind == JsonValueKind.Number
? v.GetSingle()
: fallback;
}