Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@
"peerDependencies": {
"typescript": "^5.6.2"
},
"version": "0.19.13"
"version": "0.20.0"
}
91 changes: 56 additions & 35 deletions src/lib/buy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function _registerBuy(program: Command) {
.option("-y, --yes", "Automatically confirm the order")
.option(
"-colo, --colocate <contract_id>",
"Colocate with existing contracts",
"Colocate with existing contracts. If provided, `-t`/`--type` will be ignored.",
)
.option(
"-q, --quote",
Expand All @@ -109,9 +109,11 @@ export function _registerBuy(program: Command) {
"Send into a specific cluster (deprecated, alias for --zone). If provided, \`-t\`/`--type` will be ignored.",
)
.hook("preAction", (command) => {
const { type, zone, cluster } = command.opts();
if (!type && !zone && !cluster) {
console.error(chalk.yellow("Must specify either --type or --zone"));
const { type, zone, cluster, colocate } = command.opts();
if (!type && !zone && !cluster && !colocate) {
console.error(
chalk.yellow("Must specify either --type, --zone or --colocate"),
);
command.help();
process.exit(1);
}
Expand All @@ -129,19 +131,19 @@ export function _registerBuy(program: Command) {
`
Examples:
\x1b[2m# Buy 8 H100s for 1 hour at market price\x1b[0m
$ sf buy -n 8 -d 1h
$ sf buy -t h100v -n 8 -d 1h

\x1b[2m# Buy 32 H100s for 6 hours starting in 3 hours\x1b[0m
$ sf buy -n 32 -d 6h -s +3h
$ sf buy -t h100v -n 32 -d 6h -s +3h

\x1b[2m# Buy 64 H100s for 12 hours starting tomorrow at 9am\x1b[0m
$ sf buy -n 64 -d 12h -s "tomorrow at 9am"
$ sf buy -t h100v -n 64 -d 12h -s "tomorrow at 9am"

\x1b[2m# Extend an existing contract that ends at 4pm by 4 hours\x1b[0m
$ sf buy -s 4pm -d 4h -colo <contract_id>

\x1b[2m# Place a standing order at a specific price\x1b[0m
$ sf buy -n 16 -d 24h -p 1.50 --standing
$ sf buy -t h100v -n 16 -d 24h -p 1.50 --standing
`,
)
.action(function buyOrderAction(options) {
Expand Down Expand Up @@ -275,7 +277,7 @@ export function QuoteAndBuy(props: { options: SfBuyOptions }) {
props.options;

setOrderProps({
type: type ?? "h100i", // We still need to pass something even if --zone is provided
type,
price: pricePerGpuHour,
size: accelerators / GPUS_PER_NODE,
startAt,
Expand Down Expand Up @@ -308,13 +310,7 @@ export function getTotalPrice(
return Math.ceil(pricePerGpuHour * size * GPUS_PER_NODE * durationInHours);
}

function BuyOrderPreview(props: {
price: number;
size: number;
startAt: Date | "NOW";
endsAt: Date;
type: string;
}) {
function BuyOrderPreview(props: BuyOrderProps) {
const startDate = props.startAt === "NOW" ? dayjs() : dayjs(props.startAt);
const start = startDate.format("MMM D h:mm a").toLowerCase();

Expand All @@ -332,9 +328,10 @@ function BuyOrderPreview(props: {
const totalPrice = getTotalPrice(props.price, props.size, realDurationHours) /
100;

const isSupportedType = props.type in InstanceTypeMetadata;
const isSupportedType = typeof props.type === "string" &&
props.type in InstanceTypeMetadata;
const typeLabel = isSupportedType
? InstanceTypeMetadata[props.type].displayName
? InstanceTypeMetadata[props.type!]?.displayName
: props.type;

return (
Expand Down Expand Up @@ -365,19 +362,43 @@ function BuyOrderPreview(props: {
head="size"
value={`${props.size * GPUS_PER_NODE} gpus`}
/>
<Box>
<Box width={7}>
<Text dimColor>type</Text>
{typeLabel && (
<Box>
<Box width={7}>
<Text dimColor>type</Text>
</Box>
<Box gap={1}>
<Text>{typeLabel}</Text>
{isSupportedType && (
<Text dimColor>
({props.type!})
</Text>
)}
</Box>
</Box>
<Box gap={1}>
<Text>{typeLabel}</Text>
{isSupportedType && (
<Text dimColor>
({props.type})
</Text>
)}
)}
{props.cluster && (
<Box>
<Box width={7}>
<Text dimColor>zone</Text>
</Box>
<Box gap={1}>
<Text>{props.cluster}</Text>
</Box>
</Box>
</Box>
)}
{props.colocate && (
<Box>
<Box>
<Box width={7}>
<Text dimColor>colocate with</Text>
</Box>
</Box>
<Box gap={1}>
<Text>{props.colocate}</Text>
</Box>
</Box>
)}
<Row
headWidth={7}
head="rate"
Expand All @@ -400,7 +421,7 @@ type BuyOrderProps = {
size: number;
startAt: Date | "NOW";
endsAt: Date;
type: string;
type?: string;
colocate?: string;
yes?: boolean;
standing?: boolean;
Expand Down Expand Up @@ -646,7 +667,7 @@ function BuyOrder(props: BuyOrderProps) {
}

export async function placeBuyOrder(options: {
instanceType: string;
instanceType?: string;
totalPriceInCents: number;
startsAt: Date | "NOW";
endsAt: Date;
Expand Down Expand Up @@ -681,7 +702,7 @@ export async function placeBuyOrder(options: {
}

const body = {
side: "buy",
side: "buy" as const,
instance_type: options.instanceType,
quantity: options.numberNodes,
start_at,
Expand All @@ -693,7 +714,7 @@ export async function placeBuyOrder(options: {
ioc: !options.standing,
},
cluster: options.cluster,
} as const;
};
const { data, error, response } = await api.POST("/v0/orders", {
body,
});
Expand Down Expand Up @@ -761,7 +782,7 @@ async function getQuoteFromParsedSfBuyOptions(options: SfBuyOptions) {
);

return await getQuote({
instanceType: options.type ?? "h100i", // We still need to pass something even if --zone is provided
instanceType: options.type,
quantity,
minStartTime: startsAt,
maxStartTime: startsAt,
Expand All @@ -773,7 +794,7 @@ async function getQuoteFromParsedSfBuyOptions(options: SfBuyOptions) {
}

type QuoteOptions = {
instanceType: string;
instanceType?: string;
quantity: number;
minStartTime: Date | "NOW";
maxStartTime: Date | "NOW";
Expand Down
Loading