sesterce-cli
Version:
A powerful command-line interface tool for managing Sesterce Cloud services. Sesterce CLI provides easy access to GPU cloud instances, AI inference services, container registries, and SSH key management directly from your terminal.
427 lines (373 loc) • 13.8 kB
text/typescript
import {
InferenceInstance,
InferenceInstanceFeature,
} from "@/modules/ai-inference/domain/inference-instance";
import { InferenceModel } from "@/modules/ai-inference/domain/inference-model";
import { getInstanceDetails } from "@/modules/ai-inference/use-cases/get-instance-details";
import { listInferenceHardwares } from "@/modules/ai-inference/use-cases/list-inference-hardwares";
import { listInferenceModels } from "@/modules/ai-inference/use-cases/list-inference-models";
import { listInferenceRegions } from "@/modules/ai-inference/use-cases/list-inference-regions";
import { listInferenceInstances } from "@/modules/ai-inference/use-cases/list-instances";
import { previewInstancePricing } from "@/modules/ai-inference/use-cases/preview-instance-pricing";
import { InstancePricing } from "@/modules/ai-inference/use-cases/preview-instance-pricing/preview-instance-pricing-dto";
import { updateInferenceInstance } from "@/modules/ai-inference/use-cases/update-instance";
import { Registry } from "@/modules/registries/domain/registry";
import { listRegistries } from "@/modules/registries/use-cases/list-registries";
import {
checkbox,
confirm,
editor,
input,
number,
search,
} from "@inquirer/prompts";
import type { Command } from "commander";
const printInstance = (instance: InferenceInstance) => {
return `${instance.name} (${instance.features.join(", ")}) | ${instance.address} | ${instance.status} | ${instance.hourlyPrice.toFixed(2)}`;
};
const printRegistry = (registry: Registry) => {
return `${registry.name} - ${registry.url} - ${registry.username}`;
};
export function createAIInferenceInstanceUpdateCommand(
aiInferenceInstanceCommand: Command
) {
aiInferenceInstanceCommand
.command("update")
.description("Update an AI inference instance")
.option(
"-i, --instance-id <instanceId>",
"The ID of the instance to update"
)
.action(async (args) => {
let instanceId = args.instanceId;
if (!instanceId) {
console.log("Loading inference instances...");
const inferenceInstancesResult = await listInferenceInstances.execute();
if (inferenceInstancesResult.isLeft()) {
console.error(inferenceInstancesResult.value.message);
return;
}
const instances = inferenceInstancesResult.value;
if (instances.length === 0) {
console.log("No instances found");
return;
}
const selectedInstance = await search({
message: "Select an instance to update",
source: async (input, { signal }) => {
if (!input) {
return instances.map((instance) => ({
name: printInstance(instance),
value: instance,
}));
}
return instances
.filter(
(instance) =>
instance.name.toLowerCase().includes(input.toLowerCase()) ||
instance.address
.toLowerCase()
.includes(input.toLowerCase()) ||
instance.features.includes(
input.toLowerCase() as InferenceInstanceFeature
) ||
instance.status.toLowerCase().includes(input.toLowerCase()) ||
instance.name.toLowerCase().includes(input.toLowerCase()) ||
instance.hourlyPrice.toFixed(2).includes(input)
)
.map((instance) => ({
name: printInstance(instance),
value: instance,
}));
},
});
instanceId = selectedInstance._id;
}
console.log("Loading instance details...");
const instanceDetailsResult =
await getInstanceDetails.execute(instanceId);
if (instanceDetailsResult.isLeft()) {
console.error(instanceDetailsResult.value.message);
return;
}
const instanceToUpdate = instanceDetailsResult.value;
console.log("Loading offers...");
const [hardwaresResult, regionsResult] = await Promise.all([
listInferenceHardwares.execute(),
listInferenceRegions.execute(),
]);
if (hardwaresResult.isLeft()) {
console.error(hardwaresResult.value?.message);
return;
}
if (regionsResult.isLeft()) {
console.error(regionsResult.value?.message);
return;
}
const hardwares = hardwaresResult.value;
const regions = regionsResult.value;
const answer = await checkbox({
message: "Deploy public or private model?",
choices: [
{ name: "Public model", value: "public", checked: true },
{ name: "Private model (registry)", value: "private" },
],
});
const modelType = answer[0];
let model: InferenceModel | null = null;
let registry: Registry | null = null;
if (modelType === "public") {
console.log("Loading public models...");
const modelsResult = await listInferenceModels.execute();
if (modelsResult.isLeft()) {
console.error(modelsResult.value.message);
return;
}
const models = modelsResult.value;
const selectedModel = await search({
message: "Select a model",
source: async (input, { signal }) => {
if (!input)
return models.map((model) => ({
name: model.name,
value: model,
}));
return models
.filter((model) =>
model.name.toLowerCase().includes(input.toLowerCase())
)
.map((model) => ({
name: model.name,
value: model,
}));
},
});
model = selectedModel;
} else {
console.log("Loading registries...");
const registriesResult = await listRegistries.execute();
if (registriesResult.isLeft()) {
console.error(registriesResult.value.message);
return;
}
const registries = registriesResult.value;
const selectedRegistry = await search({
message: "Select a registry",
source: async (input, { signal }) => {
if (!input)
return registries.map((registry) => ({
name: printRegistry(registry),
value: registry,
}));
return registries
.filter((registry) =>
registry.name.toLowerCase().includes(input.toLowerCase())
)
.map((registry) => ({
name: printRegistry(registry),
value: registry,
}));
},
});
registry = selectedRegistry;
}
const containerPort = await number({
message: "Port to expose the model",
required: true,
default: model?.port ?? 80,
});
const availableHardwares = hardwares.map((hardware) => ({
...hardware,
available: regions.some((region) =>
region.capacities.some(
(capacity) =>
capacity.hardwareName === hardware.name && capacity.capacity > 0
)
),
}));
const hardware = await search({
message: `Select a hardware (current: ${instanceToUpdate.hardwareName})`,
source: async (input, { signal }) => {
if (!input)
return availableHardwares.map((hardware) => ({
name: hardware.name,
value: hardware,
disabled: !hardware.available ? "(Out of stock)" : false,
}));
return availableHardwares
.filter((hardware) =>
hardware.name.toLowerCase().includes(input.toLowerCase())
)
.map((hardware) => ({
name: hardware.name,
value: hardware,
disabled: !hardware.available ? "(Out of stock)" : false,
}));
},
});
console.log("Loading regions...");
const hardwareRegions = regions.filter((region) =>
region.capacities.some(
(capacity) =>
capacity.hardwareName === hardware.name && capacity.capacity > 0
)
);
const pricingResult = await previewInstancePricing.execute({
hardwareName: hardware.name,
regions: hardwareRegions.map((region) => region.id),
maxContainers: 1,
});
if (pricingResult.isLeft()) {
console.error(pricingResult.value.message);
return;
}
const regionPricingDict = pricingResult.value.pricesPerRegion.reduce(
(acc, { regionId, ...pricing }) => {
acc[regionId] = pricing;
return acc;
},
{} as Record<number, InstancePricing>
);
const selectedRegions = await checkbox({
message: "Select one or more regions",
required: true,
choices: hardwareRegions.map((region) => ({
name: regionPricingDict[region.id]
? `${region.name} - $${regionPricingDict[region.id].pricePerHour}/hour`
: region.name,
value: region,
checked: instanceToUpdate.containers.some(
(container) => container.regionId === region.id
),
})),
});
const startupCommand = await input({
message: "Startup command",
default: instanceToUpdate.startupCommand,
});
// all containers have the same scale config, so we can use the first one
const [firstContainer] = instanceToUpdate.containers;
const minContainers = await number({
message: "Minimum number of containers (0-3)",
default: firstContainer.scale.min,
validate: (value) => {
if (value === undefined) {
return true;
}
if (value < 0 || value > 3) {
return "Minimum number of containers must be between 0 and 3";
}
return true;
},
});
const maxContainers = await number({
message: "Maximum number of containers (1-25)",
required: true,
default: firstContainer.scale.max,
validate: (value) => {
if (value === undefined) {
return true;
}
if (value < 1 || value > 25) {
return "Maximum number of containers must be between 1 and 25";
}
return true;
},
});
const cooldownPeriod = await number({
message: "Cooldown period (in seconds)",
default: firstContainer.scale.cooldownPeriod,
});
const timeout = await number({
message: "Timeout (in seconds)",
default: instanceToUpdate.podLifetime,
});
console.log("Container Deployment Triggers");
const cpu = await number({
message: "CPU usage (%)",
default: firstContainer.scale.triggers?.cpu?.threshold,
});
const gpuUtilization = await number({
message: "GPU usage (%)",
default: firstContainer.scale.triggers?.gpuUtilization?.threshold,
});
const gpuMemory = await number({
message: "GPU memory (%)",
default: firstContainer.scale.triggers?.gpuMemory?.threshold,
});
const memory = await number({
message: "RAM usage (%)",
default: firstContainer.scale.triggers?.memory?.threshold,
});
const http = await number({
message: "HTTP requests (/sec)",
default: firstContainer.scale.triggers?.http?.rate,
});
const envVars = await editor({
message: "Environment Variables",
default: Object.entries(instanceToUpdate.envs)
.map(([key, value]) => `${key}=${value}`)
.join("\n"),
});
let envs: Record<string, string> = {};
if (envVars.length > 0) {
envs = envVars.split("\n").reduce(
(acc, env) => {
const [key, value] = env.split("=");
if (key && value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
}
console.log("Calculating final price...");
const finalPrice = await previewInstancePricing.execute({
hardwareName: hardware.name,
regions: selectedRegions.map((region) => region.id),
maxContainers,
});
if (finalPrice.isLeft()) {
console.error(finalPrice.value.message);
return;
}
console.log(
`Final price: $${finalPrice.value.totalPrice.pricePerHour}/hour | $${finalPrice.value.totalPrice.pricePerMonth}/month`
);
const confirmDeployment = await confirm({
message: "Are you sure you want to deploy this instance?",
});
if (!confirmDeployment) {
console.log("Deployment cancelled");
return;
}
console.log("Updating instance...");
const updateInstanceResult = await updateInferenceInstance.execute({
instanceId,
modelId: model?.id ?? null,
registryId: registry?._id ?? null,
containerPort,
regionsIds: selectedRegions.map((region) => region.id),
minContainers: minContainers ?? 1,
maxContainers,
cooldownPeriod,
triggers: {
cpu,
gpuUtilization,
gpuMemory,
memory,
http,
},
timeout,
envs,
hardwareName: hardware.name,
startupCommand: startupCommand ?? null,
});
if (updateInstanceResult.isLeft()) {
console.error(updateInstanceResult.value.message);
return;
}
console.log("Instance updated successfully!");
});
}