UNPKG

@sschepis/vast-node

Version:

Node.js client for Vast.ai API - Rent GPUs for machine learning and AI workloads

505 lines (504 loc) 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VastClient = void 0; const dynamic_api_1 = require("./dynamic-api"); /** * VastClient * * The main client class for interacting with the Vast.ai API. * Provides methods for working with GPU instances, offers, and images. * * @example * ```typescript * // Initialize with your API key * const client = new VastClient('your-api-key'); * * // Search for available machines * const offers = await client.searchOffers({ * num_gpus: 1, * cuda_max_good: 11.5 * }); * ``` */ class VastClient { constructor(apiKey, serverUrl = 'https://console.vast.ai') { this.apiKey = null; this.apiKey = apiKey || null; // Define all the Vast.ai API endpoints const apiConfig = { baseUrl: serverUrl, endpoints: { // Offers endpoints searchOffers: { method: 'GET', path: '/api/v0/bundles', params: [ 'cudaMaxGood', 'cudaVers', 'diskSpace', 'external', 'inetDown', 'inetUp', 'minBid', 'numGpus', 'orderBy', 'q', 'verified', 'type', 'storageSize', 'reliability', 'directPortCount' ], responseType: 'json', // Add responseType retryConfig: { maxRetries: 3, retryDelay: 1000 } }, getOffer: { method: 'GET', path: '/api/v0/bundles/:id', params: ['id'], retryConfig: { maxRetries: 2, retryDelay: 500 } }, // Instances endpoints listInstances: { method: 'GET', path: '/api/v0/instances', params: ['api_key'], // Include api_key for query param ignoreGlobalAuth: true, // Ignore the global Authorization header for this endpoint retryConfig: { maxRetries: 3, retryDelay: 1000 } }, getInstance: { method: 'GET', path: '/api/v0/instances/:id', params: ['id'], retryConfig: { maxRetries: 2, retryDelay: 500 } }, createInstance: { method: 'PUT', path: '/api/v0/asks/:id', // Use the /asks endpoint with offer ID in path params: ['id', 'api_key'], // Include id for path interpolation and api_key for query param ignoreGlobalAuth: true, // Ignore the global Authorization header for this endpoint retryConfig: { maxRetries: 3, retryDelay: 1000 } }, startInstance: { method: 'PUT', path: '/api/v0/instances/:id/start', params: ['id'], retryConfig: { maxRetries: 3, retryDelay: 1000 } }, stopInstance: { method: 'PUT', path: '/api/v0/instances/:id/stop', params: ['id', 'api_key'], // Include api_key for query param ignoreGlobalAuth: true, // Ignore the global Authorization header for this endpoint retryConfig: { maxRetries: 3, retryDelay: 1000 } }, deleteInstance: { method: 'DELETE', // Use DELETE method for destroying path: '/api/v0/instances/:id', // Use the instance ID in the path params: ['id', 'api_key'], ignoreGlobalAuth: false, // Use global Bearer token by default retryConfig: { maxRetries: 3, retryDelay: 1000 } }, // Images endpoints listImages: { method: 'GET', path: '/api/v0/docker-images', params: ['api_key'], headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, responseType: 'json', retryConfig: { maxRetries: 3, retryDelay: 1000 } }, // User endpoints getUserInfo: { method: 'GET', path: '/api/v0/users/current', retryConfig: { maxRetries: 3, retryDelay: 1000 } }, }, globalHeaders: { 'Content-Type': 'application/json', }, timeout: 30000, responseInterceptor: (response) => { // Log responses in debug mode // console.log('[VastClient Response Interceptor] Status:', response.status); // console.log('[VastClient Response Interceptor] Raw Data (snake_case):', response.data); const camelCaseData = (0, dynamic_api_1.transformToCamelCase)(response.data); // console.log('[VastClient Response Interceptor] Transformed Data (camelCase):', camelCaseData); return camelCaseData; }, errorInterceptor: (error) => { console.error('Request failed:', error.message); // Add more detailed logging for API errors if (error.response) { console.error(`API Error (${error.response.status}):`, error.response.data); } else if (error.request) { console.error('Network Error - No response received'); } else { console.error('Request configuration error:', error.message); } return Promise.reject(error); }, globalRetryConfig: { maxRetries: 3, retryDelay: 1000, retryCondition: (error) => { // Retry on network errors or 5xx server errors return !error.response || error.response.status >= 500; } }, queueConcurrency: 3 }; // Create the dynamic API instance this.dynamicApi = new dynamic_api_1.DynamicApi(apiConfig); // Use imported DynamicApi // Set auth token if provided if (this.apiKey) { this.setApiKey(this.apiKey); } // Create API methods from the configuration this.api = this.dynamicApi.createApiMethods(); } /** * Set the API key for authentication * * @param apiKey - Your Vast.ai API key, obtained from the Vast.ai console * @example * ```typescript * client.setApiKey('your-new-api-key'); * ``` */ setApiKey(apiKey) { this.apiKey = apiKey; this.dynamicApi.updateGlobalHeaders({ 'Authorization': `Bearer ${apiKey}` }); } /** * Search for machine offers (available GPU instances) * * @param params - Search parameters to filter offers * @param params.cuda_max_good - Maximum CUDA version * @param params.cuda_vers - Specific CUDA version * @param params.disk_space - Minimum disk space in GB * @param params.external - Whether instance has external connectivity * @param params.inet_down - Minimum download speed in Mbps * @param params.inet_up - Minimum upload speed in Mbps * @param params.min_bid - Minimum bid price * @param params.num_gpus - Minimum number of GPUs * @param params.order - Order results (e.g., "score-" for descending score) * @param params.q - Search query string * * @returns Promise resolving to an array of machine offers * * @example * ```typescript * // Find machines with at least 2 GPUs and CUDA 11.7 * const offers = await client.searchOffers({ * num_gpus: 2, * cuda_max_good: 11.7, * order: "dph_total+" // Sort by price (cheapest first) * }); * ``` */ async searchOffers(params = {}) { console.log('Searching offers with params:', JSON.stringify(params, null, 2)); try { const responseObject = await this.api.searchOffers(params); // console.log('[VastClient searchOffers] Raw result from this.api.searchOffers (expected object with offers):', responseObject); // Extract the 'offers' array from the response object const offersArray = responseObject && responseObject.offers ? responseObject.offers : []; if (!Array.isArray(offersArray)) { console.error('Error: Expected an array of offers, but received:', offersArray); return []; // Return empty array on unexpected structure } console.log(`Found ${offersArray.length} offers`); return offersArray; } catch (error) { console.error('Error searching offers:', error); throw error; } } /** * Get a specific offer by ID * * @param id - The ID of the offer to retrieve * @returns Promise resolving to the offer details * * @example * ```typescript * const offer = await client.getOffer(12345); * ``` */ async getOffer(id) { console.log(`Getting offer with ID: ${id}`); try { const result = await this.api.getOffer({ id }); return result; } catch (error) { console.error(`Error getting offer ${id}:`, error); throw error; } } /** * List all instances for the current user * * @param params - Optional filtering parameters * @param params.owner - Filter by owner (default: current user) * @param params.q - Search query string * * @returns Promise resolving to an array of instances * * @example * ```typescript * // List all running instances * const instances = await client.listInstances({ * q: "running" * }); * ``` */ async listInstances(params = {}) { console.log('Listing instances with params:', JSON.stringify(params, null, 2)); // Include api_key in params for this specific endpoint const paramsWithApiKey = { ...params, api_key: this.apiKey // Include API key as a parameter }; try { // Pass an empty Authorization header to override the global one for this request const result = await this.api.listInstances(paramsWithApiKey, { headers: { Authorization: '' } }); console.log('Raw listInstances result:', JSON.stringify(result, null, 2)); // Log raw result // The API returns an object with an 'instances' key containing the array const instances = result && result.instances ? result.instances : []; console.log(`Found ${instances.length} instances`); return instances; // Return the array of instances } catch (error) { console.error('Error listing instances:', error); throw error; } } /** * Get a specific instance by ID * * @param id - The ID of the instance to retrieve * @returns Promise resolving to the instance details * * @example * ```typescript * const instance = await client.getInstance(12345); * ``` */ async getInstance(id) { console.log(`Getting instance with ID: ${id}`); try { const responseObject = await this.api.getInstance({ id }); // Assuming the actual instance is nested as per the test mock: result.data.instances[0] if (responseObject && responseObject.data && responseObject.data.instances && responseObject.data.instances.length > 0) { return responseObject.data.instances[0]; } // Handle cases where the structure is not as expected or instance is not found console.error(`Instance with ID ${id} not found or unexpected response structure:`, responseObject); throw new Error(`Instance with ID ${id} not found or unexpected response structure.`); } catch (error) { console.error(`Error getting instance ${id}:`, error); throw error; } } /** * Create a new instance on a machine * * @param params - Instance creation parameters * @param params.image - Docker image to use (e.g., "pytorch/pytorch:latest") * @param params.id - ID of the offer to use (obtained from searchOffers) * @param params.diskSpace - Disk space in GB * @param params.jupyterLab - Whether to enable JupyterLab * @param params.sshKeyIds - Array of SSH key IDs to add * @param params.runCommand - Command to run on startup * @param params.env - Environment variables * * @returns Promise resolving to the created instance * * @example * ```typescript * const instance = await client.createInstance({ * image: "pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime", * id: 12345, // Offer ID * diskSpace: 20, * jupyterLab: true, * env: { * JUPYTER_PASSWORD: "vastai" * } * }); * ``` */ async createInstance(params) { console.log('Creating instance with input params:', JSON.stringify(params, null, 2)); // The 'params' object is of type CreateInstanceParams. // It includes 'machineId' which is the offer ID. // The API likely expects this as 'machine_id'. const { machineId, ...otherParams } = params; const apiPayload = { ...otherParams, // Spread other parameters like image, diskSpace, etc. machine_id: machineId, // Map machineId to machine_id for the API api_key: this.apiKey }; console.log('Data object prepared for DynamicApi:', JSON.stringify(apiPayload, null, 2)); try { const responseObject = await this.api.createInstance(apiPayload, { headers: { Authorization: '' // Override global auth for this specific call if needed } }); if (responseObject && responseObject.data) { console.log('Successfully created instance:', responseObject.data.id); return responseObject.data; } console.error('Failed to create instance or unexpected API response structure:', responseObject); throw new Error('Failed to create instance or unexpected API response structure.'); } catch (error) { console.error('Error creating instance:', error); throw error; } } /** * Start an instance * * @param id - The ID of the instance to start * @returns Promise resolving to the start operation result * * @example * ```typescript * await client.startInstance(12345); * ``` */ async startInstance(id) { console.log(`Starting instance with ID: ${id}`); try { const result = await this.api.startInstance({ id }); console.log(`Instance ${id} start request successful`); return result; } catch (error) { console.error(`Error starting instance ${id}:`, error); throw error; } } /** * Stop an instance * * @param id - The ID of the instance to stop * @returns Promise resolving to the stop operation result * * @example * ```typescript * await client.stopInstance(12345); * ``` */ async stopInstance(id) { console.log(`Stopping instance with ID: ${id}`); try { const result = await this.api.stopInstance({ id, api_key: this.apiKey }, { headers: { Authorization: '' } }); console.log(`Instance ${id} stop request successful`); return result; } catch (error) { console.error(`Error stopping instance ${id}:`, error); throw error; } } /** * Delete an instance * * @param id - The ID of the instance to delete * @returns Promise resolving to the delete operation result * * @example * ```typescript * await client.deleteInstance(12345); * ``` */ async deleteInstance(id) { console.log(`[VastClient deleteInstance] Attempting to delete instance ID: ${id}. API Key: ${this.apiKey}`); const deletePayload = { id, api_key: this.apiKey }; // console.log('[VastClient deleteInstance] Payload for this.api.deleteInstance:', JSON.stringify(deletePayload)); try { // Use global auth (Bearer token), api_key will be query param const result = await this.api.deleteInstance(deletePayload); console.log(`Instance ${id} delete request successful`); return result; } catch (error) { console.error(`Error deleting instance ${id}:`, error); throw error; } } /** * List available docker images * * @param params - Optional parameters * @param params.api_key - API key to use for this request (overrides the global API key) * @returns Promise resolving to an array of available images * * @example * ```typescript * // Using the global API key * const images = await client.listImages(); * * // Or with an explicit API key * const images = await client.listImages({ api_key: 'your-api-key' }); * ``` */ async listImages(params = {}) { console.log('Listing available Docker images with params:', JSON.stringify(params, null, 2)); try { const result = await this.api.listImages(params); console.log(`Found ${result.length} Docker images`); return result; } catch (error) { console.error('Error listing Docker images:', error); throw error; } } /** * Get current user information * * @returns Promise resolving to the user information * * @example * ```typescript * const user = await client.getUserInfo(); * console.log(`Balance: $${user.balance}`); * ``` */ async getUserInfo() { console.log('Getting user information'); try { const result = await this.api.getUserInfo(); console.log(`Got user info for: ${result.username}`); return result; } catch (error) { console.error('Error getting user info:', error); throw error; } } } exports.VastClient = VastClient;