runbios-sdk 0.2.1-dev.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,419 @@
1
+ import { normalizeGPUPlacement } from './gpu-priorities.js';
2
+ const TRAINING_IDEMPOTENCY_KEY = /^[A-Za-z0-9._:-]{8,128}$/;
3
+ function trainingIdempotencyKey(explicit) {
4
+ const key = explicit?.trim() || globalThis.crypto.randomUUID();
5
+ if (!TRAINING_IDEMPOTENCY_KEY.test(key)) {
6
+ throw new Error("RunBiOS: idempotencyKey must be 8-128 characters using letters, numbers, '.', '_', ':', or '-'");
7
+ }
8
+ return key;
9
+ }
10
+ function buildTrainingRequest(params) {
11
+ let trainingMethod = params.method;
12
+ if (trainingMethod === 'vlm')
13
+ trainingMethod = 'sft';
14
+ const datasetIds = (params.datasetIds || []).filter((id) => id.trim() !== '');
15
+ if (datasetIds.length === 0 && params.datasetId?.trim())
16
+ datasetIds.push(params.datasetId);
17
+ if (datasetIds.length === 0) {
18
+ throw new Error('RunBiOS: datasetId or datasetIds must contain at least one dataset');
19
+ }
20
+ const body = {
21
+ model_id: params.model,
22
+ dataset_ids: datasetIds,
23
+ training_method: trainingMethod,
24
+ train_type: params.adapter,
25
+ };
26
+ if (params.modelRevision !== undefined)
27
+ body.model_revision = params.modelRevision;
28
+ if (params.rlhfAlgorithm !== undefined)
29
+ body.rlhf_type = params.rlhfAlgorithm;
30
+ const queueEnabled = params.queueIfUnavailable ?? false;
31
+ const placement = normalizeGPUPlacement(params.gpuPriorities, queueEnabled, params.gpuType, params.gpuCount, 'queueIfUnavailable');
32
+ body.queue_if_unavailable = queueEnabled;
33
+ if (params.queueDeadline !== undefined) {
34
+ const deadline = params.queueDeadline instanceof Date
35
+ ? params.queueDeadline
36
+ : new Date(params.queueDeadline);
37
+ if (Number.isNaN(deadline.getTime())) {
38
+ throw new Error('RunBiOS: queueDeadline must be a valid RFC 3339 timestamp');
39
+ }
40
+ body.queue_deadline = deadline.toISOString();
41
+ }
42
+ if (params.maxPriceHourCents !== undefined) {
43
+ if (!Number.isInteger(params.maxPriceHourCents) || params.maxPriceHourCents < 0) {
44
+ throw new Error('RunBiOS: maxPriceHourCents must be a non-negative integer');
45
+ }
46
+ body.max_price_hour_cents = params.maxPriceHourCents;
47
+ }
48
+ if (placement.gpuType !== undefined)
49
+ body.gpu_type = placement.gpuType;
50
+ if (placement.gpuCount !== undefined)
51
+ body.gpu_count = placement.gpuCount;
52
+ if (placement.priorities !== undefined)
53
+ body.gpu_priorities = placement.priorities;
54
+ if (params.name !== undefined)
55
+ body.name = params.name;
56
+ if (params.workspaceId !== undefined)
57
+ body.workspace_id = params.workspaceId;
58
+ if (params.storageGb !== undefined)
59
+ body.storage_gb = params.storageGb;
60
+ if (params.numCheckpoints !== undefined)
61
+ body.num_checkpoints = params.numCheckpoints;
62
+ if (params.modelParamsB !== undefined)
63
+ body.model_params_b = params.modelParamsB;
64
+ if (params.modelActiveParamsB !== undefined)
65
+ body.model_active_params_b = params.modelActiveParamsB;
66
+ if (params.integrationId !== undefined)
67
+ body.integration_id = params.integrationId;
68
+ if (params.networkVolumeId !== undefined)
69
+ body.network_volume_id = params.networkVolumeId;
70
+ if (params.cacheDataset !== undefined)
71
+ body.cache_dataset = params.cacheDataset;
72
+ if (params.datasetSampleLimits !== undefined)
73
+ body.dataset_sample_limits = params.datasetSampleLimits;
74
+ if (params.datasetMixing !== undefined)
75
+ body.dataset_mixing = params.datasetMixing;
76
+ if (params.mixing !== undefined)
77
+ body.mixing = params.mixing;
78
+ const config = {};
79
+ if (params.epochs !== undefined)
80
+ config.num_train_epochs = params.epochs;
81
+ if (params.batchSize !== undefined)
82
+ config.per_device_train_batch_size = params.batchSize;
83
+ if (params.gradientAccumulation !== undefined)
84
+ config.gradient_accumulation_steps = params.gradientAccumulation;
85
+ if (params.learningRate !== undefined)
86
+ config.learning_rate = params.learningRate;
87
+ if (params.lrScheduler !== undefined)
88
+ config.lr_scheduler_type = params.lrScheduler;
89
+ if (params.warmupRatio !== undefined)
90
+ config.warmup_ratio = params.warmupRatio;
91
+ if (params.warmupSteps !== undefined)
92
+ config.warmup_steps = params.warmupSteps;
93
+ if (params.weightDecay !== undefined)
94
+ config.weight_decay = params.weightDecay;
95
+ if (params.maxGradNorm !== undefined)
96
+ config.max_grad_norm = params.maxGradNorm;
97
+ if (params.maxSeqLength !== undefined)
98
+ config.max_length = params.maxSeqLength;
99
+ if (params.gradientCheckpointing !== undefined)
100
+ config.gradient_checkpointing = params.gradientCheckpointing;
101
+ if (params.mixedPrecision !== undefined) {
102
+ config.torch_dtype = params.mixedPrecision === 'bf16'
103
+ ? 'bfloat16'
104
+ : params.mixedPrecision === 'fp16' ? 'float16' : 'float32';
105
+ }
106
+ if (params.seed !== undefined)
107
+ config.seed = params.seed;
108
+ if (params.loraRank !== undefined)
109
+ config.lora_rank = params.loraRank;
110
+ if (params.loraAlpha !== undefined)
111
+ config.lora_alpha = params.loraAlpha;
112
+ if (params.loraDropout !== undefined)
113
+ config.lora_dropout = params.loraDropout;
114
+ if (params.loraTargetModules !== undefined)
115
+ config.target_modules = params.loraTargetModules;
116
+ if (params.quantizationBit !== undefined)
117
+ config.quant_bits = params.quantizationBit;
118
+ if (params.deepspeed !== undefined)
119
+ config.deepspeed = params.deepspeed;
120
+ if (params.saveSteps !== undefined)
121
+ config.save_steps = params.saveSteps;
122
+ if (params.saveEpochs !== undefined) {
123
+ if (params.saveEpochs !== 1) {
124
+ throw new Error('RunBiOS: saveEpochs only supports 1; use extraConfig.save_strategy for explicit checkpoint policy');
125
+ }
126
+ config.save_strategy = 'epoch';
127
+ }
128
+ if (params.maxCheckpoints !== undefined)
129
+ config.save_total_limit = params.maxCheckpoints;
130
+ if (params.evalSteps !== undefined)
131
+ config.eval_steps = params.evalSteps;
132
+ if (params.evalSplit !== undefined) {
133
+ const ratio = typeof params.evalSplit === 'string' ? Number(params.evalSplit) : params.evalSplit;
134
+ if (!Number.isFinite(ratio))
135
+ throw new Error('RunBiOS: evalSplit must be a numeric ratio');
136
+ config.split_dataset_ratio = ratio;
137
+ }
138
+ if (params.extraConfig)
139
+ Object.assign(config, params.extraConfig);
140
+ if (Object.keys(config).length > 0)
141
+ body.config = config;
142
+ return { body, datasetIds, trainingMethod };
143
+ }
144
+ function normalizeJob(raw, fallback = {}) {
145
+ const job = { ...fallback, ...raw };
146
+ const id = String(job.id || job.job_id || '');
147
+ if (!id)
148
+ throw new Error('RunBiOS: training response did not include id or job_id');
149
+ job.id = id;
150
+ if (!job.job_id && raw.job_id)
151
+ job.job_id = String(raw.job_id);
152
+ if (!job.model_id && job.model)
153
+ job.model_id = job.model;
154
+ if (!job.training_method && job.method)
155
+ job.training_method = job.method;
156
+ if (!job.train_type && job.adapter)
157
+ job.train_type = job.adapter;
158
+ if (!job.rlhf_type && job.rlhf_algorithm)
159
+ job.rlhf_type = job.rlhf_algorithm;
160
+ if (!job.dataset_ids && job.dataset_id)
161
+ job.dataset_ids = [job.dataset_id];
162
+ // Compatibility aliases let existing automations migrate without losing the
163
+ // canonical REST field names returned by the current API.
164
+ if (!job.model && job.model_id)
165
+ job.model = job.model_id;
166
+ if (!job.method && job.training_method)
167
+ job.method = job.training_method;
168
+ if (!job.adapter && job.train_type)
169
+ job.adapter = job.train_type;
170
+ if (!job.rlhf_algorithm && job.rlhf_type)
171
+ job.rlhf_algorithm = job.rlhf_type;
172
+ if (!job.dataset_id && job.dataset_ids?.length)
173
+ job.dataset_id = job.dataset_ids[0];
174
+ if (!job.error && job.error_message)
175
+ job.error = job.error_message;
176
+ if (!job.status)
177
+ job.status = 'pending';
178
+ return job;
179
+ }
180
+ /**
181
+ * Create, monitor, and manage fine-tuning training jobs.
182
+ */
183
+ export class Training {
184
+ _http;
185
+ /** @internal */
186
+ constructor(_http) {
187
+ this._http = _http;
188
+ }
189
+ /** Return the authoritative method, adapter, and hyperparameter contract Run BiOS trains against. */
190
+ async capabilities() {
191
+ return this._http.fetchGet('/api/training/capabilities');
192
+ }
193
+ /**
194
+ * Create a new training job (book-before-reveal).
195
+ *
196
+ * The call blocks while the ranked GPU ladder is booked (~40s typical). A
197
+ * training id and the "training started" email exist only once a real machine is
198
+ * secured, so the returned `status` is one of:
199
+ *
200
+ * - `"booked"` — a GPU was secured (booked == secured); the job then
201
+ * progresses through provisioning, downloading, and running on its own.
202
+ * - `"securing"` — still booking at the deadline; the async tail continues.
203
+ * Poll {@link get} until it reaches `booked`/`running` (or terminal).
204
+ * Nothing is charged until a machine is real.
205
+ * - `"queued"` — returned only with explicit queue consent
206
+ * (`queueIfUnavailable: true`); waits for stock at zero charge and books
207
+ * via the same path.
208
+ *
209
+ * If the ladder is exhausted at booking time without queue consent, this
210
+ * rejects with a `CAPACITY_UNAVAILABLE` (HTTP 409) carrying neutral
211
+ * alternatives — no phantom job remains and nothing is billed. The SDK never
212
+ * auto-substitutes a GPU; a transient 503 is a retry, never a capacity
213
+ * verdict.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * const job = await client.training.create({
218
+ * model: 'meta-llama/Llama-3.1-8B-Instruct',
219
+ * datasetId: 'ds_abc123',
220
+ * method: 'sft',
221
+ * adapter: 'lora',
222
+ * epochs: 3,
223
+ * learningRate: 2e-4,
224
+ * loraRank: 16,
225
+ * loraAlpha: 32,
226
+ * gpuType: 'A100_80GB',
227
+ * gpuCount: 1,
228
+ * });
229
+ * console.log(`Job ${job.id} created -- status: ${job.status}`);
230
+ * ```
231
+ */
232
+ async create(params) {
233
+ const { body, datasetIds, trainingMethod } = buildTrainingRequest(params);
234
+ const result = await this._http.fetchPost('/api/training/jobs', body, {
235
+ 'Idempotency-Key': trainingIdempotencyKey(params.idempotencyKey),
236
+ });
237
+ return normalizeJob(result, {
238
+ model_id: params.model,
239
+ model_revision: params.modelRevision,
240
+ training_method: trainingMethod,
241
+ train_type: params.adapter,
242
+ rlhf_type: params.rlhfAlgorithm,
243
+ dataset_ids: datasetIds,
244
+ gpu_type: params.gpuType,
245
+ gpu_count: params.gpuCount,
246
+ status: 'pending',
247
+ });
248
+ }
249
+ /** Validate and canonicalize a training request without creating or billing a job. */
250
+ async preflight(params) {
251
+ const { body } = buildTrainingRequest(params);
252
+ return this._http.fetchPost('/api/training/preflight', body);
253
+ }
254
+ /** Return one server-driven page with pagination metadata. */
255
+ async listPage(params = {}) {
256
+ const q = new URLSearchParams();
257
+ if (params.workspaceId)
258
+ q.set('workspace_id', params.workspaceId);
259
+ if (params.status)
260
+ q.set('status', params.status);
261
+ if (params.limit !== undefined)
262
+ q.set('limit', String(params.limit));
263
+ if (params.offset !== undefined)
264
+ q.set('offset', String(params.offset));
265
+ const qs = q.toString();
266
+ const raw = await this._http.fetchGet(`/api/training/jobs${qs ? `?${qs}` : ''}`);
267
+ const envelope = Array.isArray(raw)
268
+ ? { jobs: raw.map((job) => normalizeJob(job)), total: raw.length, limit: raw.length, offset: 0 }
269
+ : {
270
+ jobs: (raw.jobs || []).map((job) => normalizeJob(job)),
271
+ total: Number(raw.total || 0),
272
+ limit: Number(raw.limit || params.limit || 50),
273
+ offset: Number(raw.offset || 0),
274
+ };
275
+ return envelope;
276
+ }
277
+ /**
278
+ * List training jobs, optionally filtered by workspace or status.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * const jobs = await client.training.list({ status: 'running' });
283
+ * for (const job of jobs) {
284
+ * console.log(`${job.id} -- ${job.model} -- ${job.status}`);
285
+ * }
286
+ * ```
287
+ */
288
+ async list(params = {}) {
289
+ return (await this.listPage(params)).jobs;
290
+ }
291
+ /**
292
+ * Get detailed information about a training job.
293
+ *
294
+ * @param id - Training job ID.
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * const job = await client.training.get('job_abc123');
299
+ * console.log(`Status: ${job.status}, Progress: ${job.progress}%`);
300
+ * ```
301
+ */
302
+ async get(id) {
303
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}`);
304
+ return normalizeJob(result);
305
+ }
306
+ /**
307
+ * Get training metrics (loss curves, learning rate, throughput) for a job.
308
+ *
309
+ * @param id - Training job ID.
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * const metrics = await client.training.getMetrics('job_abc123');
314
+ * console.log(`Current loss: ${metrics.metrics.at(-1)?.loss}`);
315
+ * console.log(`${metrics.metrics.length} metric points`);
316
+ * ```
317
+ */
318
+ async getMetrics(id) {
319
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/metrics`);
320
+ const metrics = Array.isArray(result.metrics)
321
+ ? result.metrics
322
+ : Array.isArray(result.steps) ? result.steps : [];
323
+ return { ...result, metrics, graph_configs: result.graph_configs || [], steps: metrics };
324
+ }
325
+ /**
326
+ * List checkpoints saved during training.
327
+ *
328
+ * @param id - Training job ID.
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * const checkpoints = await client.training.getCheckpoints('job_abc123');
333
+ * for (const cp of checkpoints) {
334
+ * console.log(`${cp.name}: ${cp.size_bytes} bytes`);
335
+ * }
336
+ * ```
337
+ */
338
+ async getCheckpoints(id) {
339
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/checkpoints`);
340
+ return Array.isArray(result) ? result : result.checkpoints || [];
341
+ }
342
+ /**
343
+ * Get training logs for a job.
344
+ *
345
+ * @param id - Training job ID.
346
+ * @param tail - Maximum newest entries to return (server clamps to 1-1000).
347
+ *
348
+ * @example
349
+ * ```ts
350
+ * const logs = await client.training.getLogs('job_abc123');
351
+ * for (const entry of logs.logs) {
352
+ * console.log(entry.level, entry.message);
353
+ * }
354
+ * ```
355
+ */
356
+ async getLogs(id, tail = 200) {
357
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/logs?tail=${encodeURIComponent(String(tail))}`);
358
+ const logs = (result.logs || []).map((entry) => typeof entry === 'string'
359
+ ? { level: 'INFO', message: entry, timestamp: '' }
360
+ : entry);
361
+ return { logs, lines: logs.map((entry) => entry.message) };
362
+ }
363
+ /**
364
+ * Stop a running training job.
365
+ *
366
+ * By default, intermediate data (checkpoints, logs) is preserved.
367
+ * Pass `keepData: false` to clean up all job artifacts.
368
+ *
369
+ * @param id - Training job ID.
370
+ * @param keepData - Whether to keep checkpoints and logs. Defaults to true.
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * await client.training.stop('job_abc123');
375
+ * ```
376
+ */
377
+ async stop(id, keepData = true) {
378
+ return this._http.fetchPost(`/api/training/jobs/${encodeURIComponent(id)}/stop`, { keep_data: keepData });
379
+ }
380
+ /**
381
+ * Resume a stopped or `interrupted` training job from its last checkpoint.
382
+ *
383
+ * Use this on a `stopped` job, or one resting at `interrupted` — the
384
+ * self-heal state a started job enters when its machine is lost and billing has
385
+ * already been stopped (distinct from `failed`). Resume re-books on secured
386
+ * capacity before reporting resumed, so book-before-reveal still applies.
387
+ *
388
+ * @param id - Training job ID.
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * await client.training.resume('job_abc123');
393
+ * ```
394
+ */
395
+ async resume(id, idempotencyKey) {
396
+ const result = await this._http.fetchPost(`/api/training/jobs/${encodeURIComponent(id)}/resume`, undefined, { 'Idempotency-Key': trainingIdempotencyKey(idempotencyKey) });
397
+ const resumedId = result.id || result.job_id;
398
+ if (!resumedId)
399
+ throw new Error('RunBiOS: resume response did not include job_id');
400
+ return { ...result, id: resumedId };
401
+ }
402
+ /**
403
+ * Delete a specific checkpoint from a training job.
404
+ *
405
+ * @param jobId - Training job ID.
406
+ * @param checkpointId - Checkpoint ID to delete.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * await client.training.deleteCheckpoint('job_abc123', 'cp_xyz789');
411
+ * ```
412
+ */
413
+ async getEvals(id) {
414
+ return this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/evals`);
415
+ }
416
+ async deleteCheckpoint(jobId, checkpointId) {
417
+ await this._http.fetchDelete(`/api/training/jobs/${encodeURIComponent(jobId)}/checkpoints/${encodeURIComponent(checkpointId)}`);
418
+ }
419
+ }
@@ -0,0 +1,53 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { WalletBalance, TransactionListParams, TransactionListResponse } from '../types.js';
3
+ /**
4
+ * View wallet balance, transaction history, and billing information.
5
+ */
6
+ export declare class Wallet {
7
+ private readonly _http;
8
+ /** @internal */
9
+ constructor(_http: HttpClient);
10
+ /**
11
+ * Get the wallet balance, spendable balance, holds, and accruing usage.
12
+ *
13
+ * `balance_cents` is the deposited balance; `available_balance_cents` is what
14
+ * can actually be spent right now (balance minus `active_holds_cents` and
15
+ * `accruing_cents`). Spend decisions read the latter.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const balance = await client.wallet.getBalance();
20
+ * console.log(`Balance: $${(balance.balance_cents / 100).toFixed(2)}`);
21
+ * console.log(`Spendable: $${(balance.available_balance_cents / 100).toFixed(2)}`);
22
+ * console.log(`On hold: $${((balance.active_holds_cents ?? 0) / 100).toFixed(2)}`);
23
+ * console.log(`Accruing: $${((balance.accruing_cents ?? 0) / 100).toFixed(2)}`);
24
+ * ```
25
+ */
26
+ getBalance(): Promise<WalletBalance>;
27
+ /**
28
+ * List billing transactions (credits, charges, refunds).
29
+ *
30
+ * Returns the WRAPPED page `{ transactions, total, limit, offset }` -- read
31
+ * `.transactions`, never the response itself.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const page = await client.wallet.getTransactions({ limit: 20 });
36
+ * console.log(`${page.total} transactions`);
37
+ * for (const t of page.transactions) {
38
+ * console.log(`${t.type}/${t.category}: $${(t.amount_cents / 100).toFixed(2)} -- ${t.description}`);
39
+ * }
40
+ * ```
41
+ */
42
+ getTransactions(params?: TransactionListParams): Promise<TransactionListResponse>;
43
+ /**
44
+ * Get pricing information for compute and storage.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const pricing = await client.wallet.getPricing();
49
+ * console.log(pricing);
50
+ * ```
51
+ */
52
+ getPricing(): Promise<Record<string, unknown>>;
53
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * View wallet balance, transaction history, and billing information.
3
+ */
4
+ export class Wallet {
5
+ _http;
6
+ /** @internal */
7
+ constructor(_http) {
8
+ this._http = _http;
9
+ }
10
+ /**
11
+ * Get the wallet balance, spendable balance, holds, and accruing usage.
12
+ *
13
+ * `balance_cents` is the deposited balance; `available_balance_cents` is what
14
+ * can actually be spent right now (balance minus `active_holds_cents` and
15
+ * `accruing_cents`). Spend decisions read the latter.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const balance = await client.wallet.getBalance();
20
+ * console.log(`Balance: $${(balance.balance_cents / 100).toFixed(2)}`);
21
+ * console.log(`Spendable: $${(balance.available_balance_cents / 100).toFixed(2)}`);
22
+ * console.log(`On hold: $${((balance.active_holds_cents ?? 0) / 100).toFixed(2)}`);
23
+ * console.log(`Accruing: $${((balance.accruing_cents ?? 0) / 100).toFixed(2)}`);
24
+ * ```
25
+ */
26
+ async getBalance() {
27
+ return this._http.fetchGet('/api/billing/wallet');
28
+ }
29
+ /**
30
+ * List billing transactions (credits, charges, refunds).
31
+ *
32
+ * Returns the WRAPPED page `{ transactions, total, limit, offset }` -- read
33
+ * `.transactions`, never the response itself.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const page = await client.wallet.getTransactions({ limit: 20 });
38
+ * console.log(`${page.total} transactions`);
39
+ * for (const t of page.transactions) {
40
+ * console.log(`${t.type}/${t.category}: $${(t.amount_cents / 100).toFixed(2)} -- ${t.description}`);
41
+ * }
42
+ * ```
43
+ */
44
+ async getTransactions(params = {}) {
45
+ const limit = params.limit ?? 50;
46
+ const offset = params.offset ?? 0;
47
+ return this._http.fetchGet(`/api/billing/transactions?limit=${limit}&offset=${offset}`);
48
+ }
49
+ /**
50
+ * Get pricing information for compute and storage.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const pricing = await client.wallet.getPricing();
55
+ * console.log(pricing);
56
+ * ```
57
+ */
58
+ async getPricing() {
59
+ return this._http.fetchGet('/api/billing/pricing');
60
+ }
61
+ }