diff --git a/bun.lockb b/bun.lockb index 292ff99..1ad7e69 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 9a39f93..3246292 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "inquirer": "^10.1.2", "node-fetch": "^3.3.2", "openapi-fetch": "^0.11.1", - "ora": "^8.0.1", + "ora": "^8.1.0", "parse-duration": "^1.1.0" }, "devDependencies": { diff --git a/src/helpers/fetchers.ts b/src/helpers/fetchers.ts new file mode 100644 index 0000000..f13f3c7 --- /dev/null +++ b/src/helpers/fetchers.ts @@ -0,0 +1,32 @@ +import { apiClient } from "../apiClient"; +import { logAndQuit } from "./errors"; + +export async function getContract(contractId: string) { + const api = await apiClient(); + const { data, response } = await api.GET("/v0/contracts/{id}", { + params: { + path: { id: contractId }, + }, + }); + if (!response.ok) { + return logAndQuit(`Failed to get contract: ${response.statusText}`); + } + return data; +} + +export async function getOrder(orderId: string) { + const api = await apiClient(); + const { data, response, error } = await api.GET("/v0/orders/{id}", { + params: { + path: { id: orderId }, + }, + }); + if (!response.ok) { + // @ts-ignore + if (error?.code === "order.not_found") { + return null; + } + return logAndQuit(`Failed to get order: ${response.statusText}`); + } + return data; +} \ No newline at end of file diff --git a/src/helpers/price.ts b/src/helpers/price.ts new file mode 100644 index 0000000..eb634e4 --- /dev/null +++ b/src/helpers/price.ts @@ -0,0 +1,7 @@ +export function pricePerGPUHourToTotalPrice(pricePerGPUHourInCenticents: number, durationSeconds: number, nodes: number, gpusPerNode: number) { + return Math.ceil(pricePerGPUHourInCenticents * durationSeconds / 3600 * nodes * gpusPerNode); +} + +export function totalPriceToPricePerGPUHour(totalPriceInCenticents: number, durationSeconds: number, nodes: number, gpusPerNode: number) { + return totalPriceInCenticents / nodes / gpusPerNode / (durationSeconds / 3600); +} \ No newline at end of file diff --git a/src/helpers/waitingForOrder.ts b/src/helpers/waitingForOrder.ts new file mode 100644 index 0000000..a100bfd --- /dev/null +++ b/src/helpers/waitingForOrder.ts @@ -0,0 +1,23 @@ +import ora from "ora"; +import chalk from "chalk"; +import { getOrder } from "./fetchers"; +import { logAndQuit } from "./errors"; + +export async function waitForOrderToNotBePending(orderId: string) { + const spinner = ora(`Order ${orderId} - pending (this can take a moment)`).start(); + const maxTries = 25; + for (let i = 0; i < maxTries; i++) { + const order = await getOrder(orderId); + + if (order && order?.status !== "pending") { + spinner.text = `Order ${orderId} - ${order?.status}`; + spinner.succeed(); + console.log(chalk.green("Order placed successfully")); + return order; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + spinner.fail(); + logAndQuit(`Order ${orderId} - possibly failed`); +} diff --git a/src/index.ts b/src/index.ts index 899c5da..95fc00c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { registerOrders } from "./lib/orders"; import { registerSell } from "./lib/sell"; import { registerSSH } from "./lib/ssh"; import { registerTokens } from "./lib/tokens"; +import { registerDown, registerUp } from "./lib/updown"; import { registerUpgrade } from "./lib/upgrade"; const program = new Command(); @@ -32,6 +33,8 @@ registerSell(program); registerBalance(program); registerTokens(program); registerUpgrade(program); +registerUp(program); +registerDown(program); // (development commands) registerDev(program); diff --git a/src/lib/buy.ts b/src/lib/buy.ts index 558d177..913c5a7 100644 --- a/src/lib/buy.ts +++ b/src/lib/buy.ts @@ -22,13 +22,16 @@ import { } from "../helpers/units"; import type { Nullable } from "../types/empty"; import { formatDuration } from "./orders"; +import { pricePerGPUHourToTotalPrice, totalPriceToPricePerGPUHour } from "../helpers/price"; +import { GPUS_PER_NODE } from "./constants"; +import { waitForOrderToNotBePending } from "../helpers/waitingForOrder"; dayjs.extend(relativeTime); dayjs.extend(duration); interface SfBuyOptions { type: string; - nodes?: string; + accelerators?: string; duration: string; price: string; start?: string; @@ -40,10 +43,10 @@ export function registerBuy(program: Command) { program .command("buy") .description("Place a buy order") - .requiredOption("-t, --type ", "Specify the type of node") - .option("-n, --nodes ", "Specify the number of nodes") + .requiredOption("-t, --type ", "Specify the type of node", "h100i") + .option("-n, --accelerators ", "Specify the number of GPUs", "8") .requiredOption("-d, --duration ", "Specify the duration", "1h") - .option("-p, --price ", "Specify the price") + .option("-p, --price ", "The price in dollars, per GPU hour") .option("-s, --start ", "Specify the start date") .option("-y, --yes", "Automatically confirm the order") .option("--quote", "Only provide a quote for the order") @@ -66,6 +69,15 @@ async function buyOrderAction(options: SfBuyOptions) { return logAndQuit(`Invalid duration: ${options.duration}`); } + // default to 1 node if not specified + const accelerators = options.accelerators ? Number(options.accelerators) : 1; + + if (accelerators % GPUS_PER_NODE !== 0) { + const exampleCommand = `sf buy -n ${GPUS_PER_NODE} -d "${options.duration}"`; + return logAndQuit(`At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`); + } + const quantity = Math.ceil(accelerators / GPUS_PER_NODE); + // parse price let priceCenticents: Nullable = null; if (options.price) { @@ -77,6 +89,12 @@ async function buyOrderAction(options: SfBuyOptions) { priceCenticents = priceParsed; } + // Convert the price to the total price of the contract + // (price per gpu hour * gpus per node * quantity * duration in hours) + if (priceCenticents) { + priceCenticents = pricePerGPUHourToTotalPrice(priceCenticents, durationSeconds, quantity, GPUS_PER_NODE); + } + const yesFlagOmitted = options.yes === undefined || options.yes === null; const confirmWithUser = yesFlagOmitted || !options.yes; @@ -86,18 +104,12 @@ async function buyOrderAction(options: SfBuyOptions) { return logAndQuit("Invalid start date"); } - // default to 1 node if not specified - const quantity = options.nodes ? Number(options.nodes) : 1; - if (options.quote) { const quote = await getQuote({ instanceType: options.type, - priceCenticents, quantity: quantity, startsAt: startDate, durationSeconds, - confirmWithUser, - quoteOnly: isQuoteOnly, }); if (!quote) { @@ -105,19 +117,17 @@ async function buyOrderAction(options: SfBuyOptions) { } const priceLabelUsd = c.green(centicentsToDollarsFormatted(quote.price)); + const priceLabelPerGPUHour = c.green(centicentsToDollarsFormatted(totalPriceToPricePerGPUHour(quote.price, durationSeconds, quantity, GPUS_PER_NODE))); - console.log(`This order is projected to cost ${priceLabelUsd}`); + console.log(`This order is projected to cost ${priceLabelUsd} total or ${priceLabelPerGPUHour} per GPU hour`); } else { // quote if no price was provided if (!priceCenticents) { const quote = await getQuote({ instanceType: options.type, - priceCenticents, quantity: quantity, startsAt: startDate, durationSeconds, - confirmWithUser, - quoteOnly: isQuoteOnly, }); if (!quote) { @@ -142,6 +152,14 @@ async function buyOrderAction(options: SfBuyOptions) { startDate = new Date(quote.start_at); } + if (!durationSeconds) { + throw new Error("unexpectly no duration provided"); + } + + if (!priceCenticents) { + throw new Error("unexpectly no price provided"); + } + // round the start and end dates. If we came from a quote, they should already be rounded, // however, there may have been a delay between the quote and now, so we may need to move the start time up to the next minute startDate = roundStartDate(startDate); @@ -180,7 +198,12 @@ async function buyOrderAction(options: SfBuyOptions) { quoteOnly: isQuoteOnly, }); - switch (res.status) { + const order = await waitForOrderToNotBePending(res.id); + if (!order) { + return; + } + + switch (order.status) { case "pending": { const orderId = res.id; const printOrderNumber = (status: string) => @@ -269,13 +292,21 @@ function confirmPlaceOrderMessage(options: BuyOptions) { timeDescription = `from ${startAtLabel} (${c.green(fromNowTime)}) until ${endsAtLabel}`; } - const topLine = `${totalNodesLabel} ${instanceTypeLabel} ${nodesLabel} for ${c.green(durationHumanReadable)} ${timeDescription}`; + const durationInSeconds = options.endsAt.getTime() / 1000 - options.startsAt.getTime() / 1000; + const pricePerGPUHour = totalPriceToPricePerGPUHour(options.priceCenticents, durationInSeconds, options.quantity, GPUS_PER_NODE); + const pricePerHourLabel = c.green(centicentsToDollarsFormatted(pricePerGPUHour)); + + const topLine = `${totalNodesLabel} ${instanceTypeLabel} ${nodesLabel} (${GPUS_PER_NODE * options.quantity} GPUs) at ${pricePerHourLabel} per GPU hour for ${c.green(durationHumanReadable)} ${timeDescription}`; const dollarsLabel = c.green( - centicentsToDollarsFormatted(options.priceCenticents), + centicentsToDollarsFormatted( + pricePerGPUHour, + ), ); - const priceLine = `\nBuy for ${dollarsLabel}?`; + const gpusLabel = c.green(options.quantity * GPUS_PER_NODE); + + const priceLine = `\nBuy ${gpusLabel} GPUs at ${dollarsLabel} per GPU hour?`; return `${topLine}\n${priceLine} `; } @@ -327,14 +358,11 @@ export async function placeBuyOrder(options: BuyOptions) { type QuoteOptions = { instanceType: string; - priceCenticents: Nullable; quantity: number; startsAt: Date; durationSeconds: number; - confirmWithUser: boolean; - quoteOnly: boolean; }; -async function getQuote(options: QuoteOptions) { +export async function getQuote(options: QuoteOptions) { const api = await apiClient(); const { data, error, response } = await api.GET("/v0/quote", { diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..bb10428 --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1 @@ +export const GPUS_PER_NODE = 8; \ No newline at end of file diff --git a/src/lib/sell.ts b/src/lib/sell.ts index f7ad3d4..a404f8b 100644 --- a/src/lib/sell.ts +++ b/src/lib/sell.ts @@ -15,19 +15,25 @@ import { roundStartDate, } from "../helpers/units"; import type { PlaceSellOrderParameters } from "./orders"; +import { GPUS_PER_NODE } from "./constants"; +import { pricePerGPUHourToTotalPrice } from "../helpers/price"; +import ora from "ora"; +import chalk from "chalk"; +import { getContract, getOrder } from "../helpers/fetchers"; +import { waitForOrderToNotBePending } from "../helpers/waitingForOrder"; export function registerSell(program: Command) { program .command("sell") .description("Place a sell order") - .requiredOption("-p, --price ", "Specify the price in centicents") + .requiredOption("-p, --price ", "The price in dollars, per GPU hour") .requiredOption("-c, --contract-id ", "Specify the contract ID") - .option("-n, --nodes ", "Specify the number of nodes") - .requiredOption( + .option("-n, --accelerators ", "Specify the number of GPUs", "8") + .option( "-s, --start ", "Specify the start time (ISO 8601 format)", ) - .requiredOption( + .option( "-d, --duration ", "Specify the duration in seconds", ) @@ -48,50 +54,92 @@ function forceAsNumber(value: string | number): number { return Number.parseFloat(value); } + +function contractStartAndEnd(contract: { + shape: { + intervals: string[] // date strings + quantities: number[] + } +}) { + const startDate = dayjs(contract.shape.intervals[0]).toDate(); + const endDate = dayjs(contract.shape.intervals[contract.shape.intervals.length - 1]).toDate(); + + return { startDate, endDate }; +} + async function placeSellOrder(options: { price: number; contractId: string; - nodes: number; + accelerators: number; start?: string; - duration: string; - flags?: Record; + duration?: string; }) { const loggedIn = await isLoggedIn(); if (!loggedIn) { return logLoginMessageAndQuit(); } - const flags = options.flags || {}; - const durationSecs = parseDuration(options.duration, "s"); - if (!durationSecs) { - return logAndQuit("Invalid duration"); + const { centicents: priceCenticents, invalid } = priceWholeToCenticents( + options.price, + ); + if (invalid || !priceCenticents) { + return logAndQuit(`Invalid price: ${options.price}`); + } + + const contract = await getContract(options.contractId); + if (!contract) { + return logAndQuit(`Contract ${options.contractId} not found`); + } + + if (contract?.status === "pending") { + return logAndQuit(`Contract ${options.contractId} is currently pending. Please try again in a few seconds.`); } - let startDate = options.start ? chrono.parseDate(options.start) : new Date(); + if (options.accelerators % GPUS_PER_NODE !== 0) { + const exampleCommand = `sf sell -n ${GPUS_PER_NODE} -c ${options.contractId}`; + return logAndQuit(`At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`); + } + + const { startDate: contractStartDate, endDate: contractEndDate } = contractStartAndEnd({ + shape: { + intervals: contract.shape.intervals, + quantities: contract.shape.quantities, + } + }); + + let startDate = options.start ? chrono.parseDate(options.start) : contractStartDate; if (!startDate) { return logAndQuit("Invalid start date"); } startDate = roundStartDate(startDate); - let endDate = dayjs(startDate).add(durationSecs, "s").toDate(); - endDate = roundEndDate(endDate); + let endDate = contractEndDate; + if (options.duration) { + const durationSecs = parseDuration(options.duration, "s"); + if (!durationSecs) { + return logAndQuit("Invalid duration"); + } + endDate = dayjs(startDate).add(durationSecs, "s").toDate(); + } - const { centicents: priceCenticents, invalid } = priceWholeToCenticents( - options.price, - ); - if (invalid || !priceCenticents) { - return logAndQuit(`Invalid price: ${options.price}`); + endDate = roundEndDate(endDate); + // if the end date is longer than the contract, use the contract end date + if (endDate > contractEndDate) { + endDate = roundEndDate(contractEndDate); } + const totalDurationSecs = dayjs(endDate).diff(startDate, "s"); + const nodes = Math.ceil(options.accelerators / GPUS_PER_NODE); + + const totalPrice = pricePerGPUHourToTotalPrice(priceCenticents, totalDurationSecs, nodes, GPUS_PER_NODE); const params: PlaceSellOrderParameters = { side: "sell", - quantity: forceAsNumber(options.nodes), - price: priceCenticents, + quantity: forceAsNumber(options.accelerators) / GPUS_PER_NODE, + price: totalPrice, contract_id: options.contractId, start_at: startDate.toISOString(), end_at: endDate.toISOString(), - ...flags, }; const api = await apiClient(); @@ -113,6 +161,10 @@ async function placeSellOrder(options: { } } - console.log(data); + if (!data?.id) { + return logAndQuit("Order ID not found"); + } + + await waitForOrderToNotBePending(data.id); process.exit(0); } diff --git a/src/lib/updown.ts b/src/lib/updown.ts new file mode 100644 index 0000000..5bc834e --- /dev/null +++ b/src/lib/updown.ts @@ -0,0 +1,280 @@ +import { confirm } from "@inquirer/prompts"; +import c from "chalk"; +import type { Command } from "commander"; +import parseDuration from "parse-duration"; +import { apiClient } from "../apiClient"; +import { logAndQuit } from "../helpers/errors"; +import { centicentsToDollarsFormatted } from "../helpers/units"; +import { getBalance } from "./balance"; +import { getQuote } from "./buy"; +import { formatDuration } from "./orders"; + +export function registerUp(program: Command) { + const cmd = program + .command("up") + .description("Automatically buy nodes until you have the desired quantity") + .option( + "-n ", + "The number of nodes to purchase continuously", + "1", + ) + .option("-t, --type ", "Specify the type of node", "h100i") + .option("-d, --duration ", "Specify the minimum duration") + .option( + "-p, --price ", + "Specify the maximum price per node per hour", + ); + + cmd.action(async (options) => { + up(options); + }); +} + +export function registerDown(program: Command) { + const cmd = program + .command("down") + .description("Turn off nodes") + .option("-t, --type ", "Specify the type of node", "h100i"); + + cmd.action(async (options) => { + down(options); + }); +} + +const DEFAULT_PRICE_PER_NODE_HOUR_IN_CENTICENTS = 2.65 * 8 * 10_000; + +async function getDefaultProcurementOptions(props: { + duration?: string; + n?: string; + pricePerNodeHour?: string; + type?: string; +}) { + // Minimum block duration is 2 hours + // which is a bit of a smoother experience (we might want to increase this) + const duration = props.duration ?? "2h"; + let durationHours = parseDuration(duration, "h"); + if (!durationHours) { + logAndQuit(`Failed to parse duration: ${duration}`); + } + durationHours = Math.ceil(durationHours); + const n = Number.parseInt(props.n ?? "1"); + const type = props.type ?? "h100i"; + + const quote = await getQuote({ + instanceType: type, + quantity: n, + // Start immediately + startsAt: new Date(), + durationSeconds: durationHours * 60 * 60, + }); + + // Eventually we should replace this price with yesterday's index price + let quotePrice = DEFAULT_PRICE_PER_NODE_HOUR_IN_CENTICENTS; + if (quote) { + // per hour price + quotePrice = quote.price / durationHours; + } + + const pricePerNodeHourInDollars = props.pricePerNodeHour + ? Number.parseInt(props.pricePerNodeHour) + : quotePrice; + const pricePerNodeHourInCenticents = Math.ceil(pricePerNodeHourInDollars); + + const totalPriceInCenticents = + pricePerNodeHourInCenticents * + Number.parseInt(props.n ?? "1") * + durationHours; + + return { + durationHours, + pricePerNodeHourInCenticents, + n, + type, + totalPriceInCenticents, + }; +} + +// Instruct the user to set a price that's lower +function getSuggestedCommandWhenBalanceLow(props: { + durationHours: number; + pricePerNodeHourInCenticents: number; + n: number; + totalPriceInCenticents: number; + balance: number; +}) { + const affordablePrice = props.balance / 100 / (props.n * props.durationHours); + + const cmd = `sf up -n ${props.n} -d ${props.durationHours}h -p ${affordablePrice.toFixed(2)}`; + return `You could try setting a lower price and your nodes will turn on\nif the market price dips this low:\n\n\t${cmd}\n`; +} + +function confirmPlaceOrderMessage(options: { + durationHours: number; + pricePerNodeHourInCenticents: number; + n: number; + totalPriceInCenticents: number; + type: string; +}) { + const totalNodesLabel = c.green(options.n); + const instanceTypeLabel = c.green(options.type); + const nodesLabel = options.n > 1 ? "nodes" : "node"; + const durationInMilliseconds = options.durationHours * 60 * 60 * 1000; + + const timeDescription = `starting ${c.green("ASAP")} until you turn it off`; + + const topLine = `Turning on ${totalNodesLabel} ${instanceTypeLabel} ${nodesLabel} continuously for ${c.green(formatDuration(durationInMilliseconds))} ${timeDescription}`; + + const dollarsLabel = c.green( + centicentsToDollarsFormatted(options.pricePerNodeHourInCenticents), + ); + + const priceLine = `\n Pay ${dollarsLabel} per node hour?`; + + return `${topLine}\n${priceLine} `; +} + +async function up(props: { + n: string; + type: string; + duration?: string; + price?: string; + y: boolean; +}) { + const client = await apiClient(); + + const { + durationHours, + n, + type, + pricePerNodeHourInCenticents, + totalPriceInCenticents, + } = await getDefaultProcurementOptions(props); + + if (durationHours && durationHours < 1) { + console.error("Minimum duration is 1 hour"); + return; + } + + if (!props.y) { + const confirmationMessage = confirmPlaceOrderMessage({ + durationHours, + pricePerNodeHourInCenticents, + n, + totalPriceInCenticents, + type, + }); + const confirmed = await confirm({ + message: confirmationMessage, + default: false, + }); + + if (!confirmed) { + logAndQuit("Order cancelled"); + } + } + + const balance = await getBalance(); + + if (balance.available.centicents < totalPriceInCenticents) { + console.log( + `You can't afford this. Available balance: $${(balance.available.centicents / 1000000).toFixed(2)}, Minimum price: $${(totalPriceInCenticents / 1000000).toFixed(2)}\n`, + ); + const cmd = getSuggestedCommandWhenBalanceLow({ + durationHours, + pricePerNodeHourInCenticents, + n, + totalPriceInCenticents, + balance: balance.available.whole, + }); + console.log(cmd); + return; + } + + // check if there's already a procurement like this + const procurements = await client.GET("/v0/procurements"); + if (!procurements.response.ok) { + console.error(procurements.error?.message, procurements.error?.details); + throw new Error("Failed to list procurements"); + } + + for (const procurement of procurements.data?.data ?? []) { + // Currently instance groups are the same name as the instance type + // in the future they might be different. + if (procurement.instance_group === props.type) { + const res = await client.PUT("/v0/procurements/{id}", { + params: { + path: { + id: procurement.id, + }, + }, + body: { + quantity: n, + + // we only update the duration & price if it's set + min_duration_in_hours: props.duration ? durationHours : undefined, + max_price_per_node_hour: props.price + ? pricePerNodeHourInCenticents + : undefined, + }, + }); + return res.data; + } + } + + const res = await client.POST("/v0/procurements", { + body: { + instance_type: type, + quantity: n, + max_price_per_node_hour: pricePerNodeHourInCenticents, + min_duration_in_hours: Math.max(durationHours, 1), + }, + }); + + if (!res.response.ok) { + console.error(res.error?.message, res.error?.details); + throw new Error("Failed to purchase nodes"); + } + + return res.data; +} + +async function down(props: { + type: string; +}) { + const client = await apiClient(); + + // check if there's already a procurement like this + const procurements = await client.GET("/v0/procurements"); + if (!procurements.response.ok) { + console.error(procurements.error?.message, procurements.error?.details); + throw new Error("Failed to list procurements"); + } + + const procurement = procurements.data?.data.find( + (p: any) => p.instance_group === props.type, + ); + + if (!procurement) { + console.error(`No procurement found for ${props.type}`); + return; + } + + const res = await client.PUT("/v0/procurements/{id}", { + params: { + path: { + id: procurement.id, + }, + }, + body: { + quantity: 0, + block_duration_in_hours: 0, + }, + }); + + if (!res.response.ok) { + console.error(res.error?.message, res.error?.details); + throw new Error("Failed to turn off nodes"); + } + + return res.data; +} diff --git a/src/schema.ts b/src/schema.ts index aeaf5ff..1fd22fc 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -164,6 +164,38 @@ export interface paths { patch?: never; trace?: never; }; + "/v0/procurements": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getV0Procurements"]; + put?: never; + post: operations["postV0Procurements"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v0/procurements/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getV0ProcurementsById"]; + put: operations["putV0ProcurementsById"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1455,6 +1487,7 @@ export interface operations { }; }; responses: { + /** @description SSH credentials and associated Linux user to be set up on the VM when it spins up. */ 200: { headers: { [name: string]: unknown; @@ -2021,4 +2054,426 @@ export interface operations { }; }; }; + getV0Procurements: { + parameters: { + query?: never; + header?: { + /** @description Generate a bearer token with `$ sf tokens create`. */ + authorization?: string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data: { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }[]; + /** @constant */ + object: "list"; + }; + "multipart/form-data": { + data: { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }[]; + /** @constant */ + object: "list"; + }; + "text/plain": { + data: { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }[]; + /** @constant */ + object: "list"; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + }; + }; + }; + }; + postV0Procurements: { + parameters: { + query?: never; + header?: { + /** @description Generate a bearer token with `$ sf tokens create`. */ + authorization?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The instance type. */ + instance_type: string; + quantity: number; + max_price_per_node_hour: number; + block_duration_in_hours: number; + }; + "multipart/form-data": { + /** @description The instance type. */ + instance_type: string; + quantity: number; + max_price_per_node_hour: number; + block_duration_in_hours: number; + }; + "text/plain": { + /** @description The instance type. */ + instance_type: string; + quantity: number; + max_price_per_node_hour: number; + block_duration_in_hours: number; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + "multipart/form-data": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + "text/plain": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + }; + }; + }; + }; + getV0ProcurementsById: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + "multipart/form-data": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + "text/plain": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + }; + }; + }; + }; + putV0ProcurementsById: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + quantity?: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price?: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours?: number; + }; + "multipart/form-data": { + quantity?: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price?: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours?: number; + }; + "text/plain": { + quantity?: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price?: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours?: number; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + "multipart/form-data": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + "text/plain": { + id: string; + /** @description The instance group of the procurement */ + instance_group: string; + /** @description The quantity of the procurement */ + quantity: number; + /** @description The TOTAL price (in centicents) to buy the duration */ + max_price: number; + /** @description The block duration of the procurement in hours */ + min_duration_in_hours: number; + /** @description The instance type. */ + instance_type: string; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + }; + }; + }; + }; }