bios-sdk 0.1.0-dev.1

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,115 @@
1
+ /**
2
+ * Access the BIOS model catalog -- search HuggingFace models, fetch
3
+ * training-relevant configuration, and check adapter compatibility.
4
+ */
5
+ export class Models {
6
+ _http;
7
+ /** @internal */
8
+ constructor(_http) {
9
+ this._http = _http;
10
+ }
11
+ /**
12
+ * Search the HuggingFace model catalog for fine-tunable models.
13
+ *
14
+ * Results are filtered to exclude quantized derivatives (GGUF, GPTQ, AWQ)
15
+ * and adapters by default -- pass `kind: "adapter"` to find LoRA adapters.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const res = await client.models.search({ query: 'llama', type: 'llm', limit: 10 });
20
+ * for (const m of res.models) {
21
+ * console.log(`${m.id} -- ${m.totalParams}B params`);
22
+ * }
23
+ * ```
24
+ */
25
+ async search(params = {}) {
26
+ const q = new URLSearchParams();
27
+ if (params.query)
28
+ q.set('q', params.query);
29
+ if (params.type)
30
+ q.set('type', params.type);
31
+ if (params.offset !== undefined)
32
+ q.set('offset', String(params.offset));
33
+ if (params.limit !== undefined)
34
+ q.set('limit', String(params.limit));
35
+ if (params.minParams !== undefined)
36
+ q.set('min_params', String(params.minParams));
37
+ if (params.maxParams !== undefined)
38
+ q.set('max_params', String(params.maxParams));
39
+ if (params.sort)
40
+ q.set('sort', params.sort);
41
+ if (params.author)
42
+ q.set('author', params.author);
43
+ if (params.visibility)
44
+ q.set('visibility', params.visibility);
45
+ if (params.kind)
46
+ q.set('kind', params.kind);
47
+ if (params.integrationId)
48
+ q.set('integration_id', params.integrationId);
49
+ return this._http.fetchGet(`/api/public/model-search?${q}`);
50
+ }
51
+ /**
52
+ * Fetch the training configuration for a specific model from HuggingFace.
53
+ *
54
+ * Returns parameter counts, architecture type, and whether the model
55
+ * uses Mixture-of-Experts -- information needed to choose the right
56
+ * GPU and adapter configuration.
57
+ *
58
+ * @param modelId - Full HuggingFace model ID (e.g. "meta-llama/Llama-3.1-8B").
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const config = await client.models.getConfig('meta-llama/Llama-3.1-8B');
63
+ * console.log(`${config.totalParams}B params, MoE: ${config.isMoE}`);
64
+ * ```
65
+ */
66
+ async getConfig(modelId) {
67
+ return this._http.fetchGet(`/api/public/model-config?id=${encodeURIComponent(modelId)}`);
68
+ }
69
+ /**
70
+ * Check which adapters are compatible with a given architecture,
71
+ * training method, and (optionally) RLHF algorithm.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const compat = await client.models.getAdapterCompatibility({
76
+ * modelType: 'llama',
77
+ * trainingMethod: 'rlhf',
78
+ * rlhfAlgorithm: 'dpo',
79
+ * });
80
+ * const usable = compat.adapters.filter(a => a.compatible);
81
+ * console.log(`${usable.length} compatible adapters`);
82
+ * ```
83
+ */
84
+ async getAdapterCompatibility(params = {}) {
85
+ const q = new URLSearchParams();
86
+ if (params.architecture)
87
+ q.set('architecture', params.architecture);
88
+ if (params.modelType)
89
+ q.set('model_type', params.modelType);
90
+ if (params.trainingMethod)
91
+ q.set('training_method', params.trainingMethod);
92
+ if (params.rlhfAlgorithm)
93
+ q.set('rlhf_algorithm', params.rlhfAlgorithm);
94
+ return this._http.fetchGet(`/api/public/adapter-compatibility?${q}`);
95
+ }
96
+ /**
97
+ * List the architectures the platform supports for a given scope. This is
98
+ * the authoritative registry the platform gates on:
99
+ * - `inference` — HF architecture classes that can be served (deployed).
100
+ * - `training` — architecture keys the fine-tuning wizard/gate accepts.
101
+ *
102
+ * A `count` of 0 means the registry is empty and nothing is explicitly
103
+ * restricted (every architecture the engine supports is allowed).
104
+ *
105
+ * ```ts
106
+ * const { architectures } = await client.models.getSupportedArchitectures({ scope: 'inference' });
107
+ * const servable = architectures.filter(a => a.enabled).map(a => a.architecture);
108
+ * ```
109
+ */
110
+ async getSupportedArchitectures(params = {}) {
111
+ const q = new URLSearchParams();
112
+ q.set('scope', params.scope ?? 'inference');
113
+ return this._http.fetchGet(`/api/public/serving-architectures?${q}`);
114
+ }
115
+ }
@@ -0,0 +1,146 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { TrainingCreateParams, TrainingListParams, TrainingJob, TrainingListResponse, TrainingMetrics, TrainingCheckpoint, TrainingLogs, TrainingStopResponse, TrainingResumeResponse, TrainingPreflightResponse, TrainingCapabilities } from '../types.js';
3
+ /**
4
+ * Create, monitor, and manage fine-tuning training jobs.
5
+ */
6
+ export declare class Training {
7
+ private readonly _http;
8
+ /** @internal */
9
+ constructor(_http: HttpClient);
10
+ /** Return the authoritative BIOS image, method, adapter, and hyperparameter contract. */
11
+ capabilities(): Promise<TrainingCapabilities>;
12
+ /**
13
+ * Create a new training job.
14
+ *
15
+ * Provisions GPU resources, downloads the model and dataset, and begins
16
+ * training. The job starts in "pending" status and progresses through
17
+ * provisioning, downloading, and running states.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const job = await client.training.create({
22
+ * model: 'meta-llama/Llama-3.1-8B-Instruct',
23
+ * datasetId: 'ds_abc123',
24
+ * method: 'sft',
25
+ * adapter: 'lora',
26
+ * epochs: 3,
27
+ * learningRate: 2e-4,
28
+ * loraRank: 16,
29
+ * loraAlpha: 32,
30
+ * gpuType: 'A100_80GB',
31
+ * gpuCount: 1,
32
+ * });
33
+ * console.log(`Job ${job.id} created -- status: ${job.status}`);
34
+ * ```
35
+ */
36
+ create(params: TrainingCreateParams): Promise<TrainingJob>;
37
+ /** Validate and canonicalize a training request without creating or billing a job. */
38
+ preflight(params: TrainingCreateParams): Promise<TrainingPreflightResponse>;
39
+ /** Return one server-driven page with pagination metadata. */
40
+ listPage(params?: TrainingListParams): Promise<TrainingListResponse>;
41
+ /**
42
+ * List training jobs, optionally filtered by workspace or status.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const jobs = await client.training.list({ status: 'running' });
47
+ * for (const job of jobs) {
48
+ * console.log(`${job.id} -- ${job.model} -- ${job.status}`);
49
+ * }
50
+ * ```
51
+ */
52
+ list(params?: TrainingListParams): Promise<TrainingJob[]>;
53
+ /**
54
+ * Get detailed information about a training job.
55
+ *
56
+ * @param id - Training job ID.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const job = await client.training.get('job_abc123');
61
+ * console.log(`Status: ${job.status}, Progress: ${job.progress}%`);
62
+ * ```
63
+ */
64
+ get(id: string): Promise<TrainingJob>;
65
+ /**
66
+ * Get training metrics (loss curves, learning rate, throughput) for a job.
67
+ *
68
+ * @param id - Training job ID.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const metrics = await client.training.getMetrics('job_abc123');
73
+ * console.log(`Current loss: ${metrics.metrics.at(-1)?.loss}`);
74
+ * console.log(`${metrics.metrics.length} metric points`);
75
+ * ```
76
+ */
77
+ getMetrics(id: string): Promise<TrainingMetrics>;
78
+ /**
79
+ * List checkpoints saved during training.
80
+ *
81
+ * @param id - Training job ID.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const checkpoints = await client.training.getCheckpoints('job_abc123');
86
+ * for (const cp of checkpoints) {
87
+ * console.log(`${cp.name}: ${cp.size_bytes} bytes`);
88
+ * }
89
+ * ```
90
+ */
91
+ getCheckpoints(id: string): Promise<TrainingCheckpoint[]>;
92
+ /**
93
+ * Get training logs for a job.
94
+ *
95
+ * @param id - Training job ID.
96
+ * @param tail - Maximum newest entries to return (server clamps to 1-1000).
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const logs = await client.training.getLogs('job_abc123');
101
+ * for (const entry of logs.logs) {
102
+ * console.log(entry.level, entry.message);
103
+ * }
104
+ * ```
105
+ */
106
+ getLogs(id: string, tail?: number): Promise<TrainingLogs>;
107
+ /**
108
+ * Stop a running training job.
109
+ *
110
+ * By default, intermediate data (checkpoints, logs) is preserved.
111
+ * Pass `keepData: false` to clean up all job artifacts.
112
+ *
113
+ * @param id - Training job ID.
114
+ * @param keepData - Whether to keep checkpoints and logs. Defaults to true.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * await client.training.stop('job_abc123');
119
+ * ```
120
+ */
121
+ stop(id: string, keepData?: boolean): Promise<TrainingStopResponse>;
122
+ /**
123
+ * Resume a stopped training job from its last checkpoint.
124
+ *
125
+ * @param id - Training job ID.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * await client.training.resume('job_abc123');
130
+ * ```
131
+ */
132
+ resume(id: string, idempotencyKey?: string): Promise<TrainingResumeResponse>;
133
+ /**
134
+ * Delete a specific checkpoint from a training job.
135
+ *
136
+ * @param jobId - Training job ID.
137
+ * @param checkpointId - Checkpoint ID to delete.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * await client.training.deleteCheckpoint('job_abc123', 'cp_xyz789');
142
+ * ```
143
+ */
144
+ getEvals(id: string): Promise<Record<string, unknown>>;
145
+ deleteCheckpoint(jobId: string, checkpointId: string): Promise<void>;
146
+ }
@@ -0,0 +1,399 @@
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("BiOS: 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('BiOS: 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('BiOS: 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('BiOS: 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('BiOS: 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('BiOS: 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('BiOS: 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 BIOS image, method, adapter, and hyperparameter contract. */
190
+ async capabilities() {
191
+ return this._http.fetchGet('/api/training/capabilities');
192
+ }
193
+ /**
194
+ * Create a new training job.
195
+ *
196
+ * Provisions GPU resources, downloads the model and dataset, and begins
197
+ * training. The job starts in "pending" status and progresses through
198
+ * provisioning, downloading, and running states.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const job = await client.training.create({
203
+ * model: 'meta-llama/Llama-3.1-8B-Instruct',
204
+ * datasetId: 'ds_abc123',
205
+ * method: 'sft',
206
+ * adapter: 'lora',
207
+ * epochs: 3,
208
+ * learningRate: 2e-4,
209
+ * loraRank: 16,
210
+ * loraAlpha: 32,
211
+ * gpuType: 'A100_80GB',
212
+ * gpuCount: 1,
213
+ * });
214
+ * console.log(`Job ${job.id} created -- status: ${job.status}`);
215
+ * ```
216
+ */
217
+ async create(params) {
218
+ const { body, datasetIds, trainingMethod } = buildTrainingRequest(params);
219
+ const result = await this._http.fetchPost('/api/training/jobs', body, {
220
+ 'Idempotency-Key': trainingIdempotencyKey(params.idempotencyKey),
221
+ });
222
+ return normalizeJob(result, {
223
+ model_id: params.model,
224
+ model_revision: params.modelRevision,
225
+ training_method: trainingMethod,
226
+ train_type: params.adapter,
227
+ rlhf_type: params.rlhfAlgorithm,
228
+ dataset_ids: datasetIds,
229
+ gpu_type: params.gpuType,
230
+ gpu_count: params.gpuCount,
231
+ status: 'pending',
232
+ });
233
+ }
234
+ /** Validate and canonicalize a training request without creating or billing a job. */
235
+ async preflight(params) {
236
+ const { body } = buildTrainingRequest(params);
237
+ return this._http.fetchPost('/api/training/preflight', body);
238
+ }
239
+ /** Return one server-driven page with pagination metadata. */
240
+ async listPage(params = {}) {
241
+ const q = new URLSearchParams();
242
+ if (params.workspaceId)
243
+ q.set('workspace_id', params.workspaceId);
244
+ if (params.status)
245
+ q.set('status', params.status);
246
+ if (params.limit !== undefined)
247
+ q.set('limit', String(params.limit));
248
+ if (params.offset !== undefined)
249
+ q.set('offset', String(params.offset));
250
+ const qs = q.toString();
251
+ const raw = await this._http.fetchGet(`/api/training/jobs${qs ? `?${qs}` : ''}`);
252
+ const envelope = Array.isArray(raw)
253
+ ? { jobs: raw.map((job) => normalizeJob(job)), total: raw.length, limit: raw.length, offset: 0 }
254
+ : {
255
+ jobs: (raw.jobs || []).map((job) => normalizeJob(job)),
256
+ total: Number(raw.total || 0),
257
+ limit: Number(raw.limit || params.limit || 50),
258
+ offset: Number(raw.offset || 0),
259
+ };
260
+ return envelope;
261
+ }
262
+ /**
263
+ * List training jobs, optionally filtered by workspace or status.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * const jobs = await client.training.list({ status: 'running' });
268
+ * for (const job of jobs) {
269
+ * console.log(`${job.id} -- ${job.model} -- ${job.status}`);
270
+ * }
271
+ * ```
272
+ */
273
+ async list(params = {}) {
274
+ return (await this.listPage(params)).jobs;
275
+ }
276
+ /**
277
+ * Get detailed information about a training job.
278
+ *
279
+ * @param id - Training job ID.
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * const job = await client.training.get('job_abc123');
284
+ * console.log(`Status: ${job.status}, Progress: ${job.progress}%`);
285
+ * ```
286
+ */
287
+ async get(id) {
288
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}`);
289
+ return normalizeJob(result);
290
+ }
291
+ /**
292
+ * Get training metrics (loss curves, learning rate, throughput) for a job.
293
+ *
294
+ * @param id - Training job ID.
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * const metrics = await client.training.getMetrics('job_abc123');
299
+ * console.log(`Current loss: ${metrics.metrics.at(-1)?.loss}`);
300
+ * console.log(`${metrics.metrics.length} metric points`);
301
+ * ```
302
+ */
303
+ async getMetrics(id) {
304
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/metrics`);
305
+ const metrics = Array.isArray(result.metrics)
306
+ ? result.metrics
307
+ : Array.isArray(result.steps) ? result.steps : [];
308
+ return { ...result, metrics, graph_configs: result.graph_configs || [], steps: metrics };
309
+ }
310
+ /**
311
+ * List checkpoints saved during training.
312
+ *
313
+ * @param id - Training job ID.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * const checkpoints = await client.training.getCheckpoints('job_abc123');
318
+ * for (const cp of checkpoints) {
319
+ * console.log(`${cp.name}: ${cp.size_bytes} bytes`);
320
+ * }
321
+ * ```
322
+ */
323
+ async getCheckpoints(id) {
324
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/checkpoints`);
325
+ return Array.isArray(result) ? result : result.checkpoints || [];
326
+ }
327
+ /**
328
+ * Get training logs for a job.
329
+ *
330
+ * @param id - Training job ID.
331
+ * @param tail - Maximum newest entries to return (server clamps to 1-1000).
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * const logs = await client.training.getLogs('job_abc123');
336
+ * for (const entry of logs.logs) {
337
+ * console.log(entry.level, entry.message);
338
+ * }
339
+ * ```
340
+ */
341
+ async getLogs(id, tail = 200) {
342
+ const result = await this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/logs?tail=${encodeURIComponent(String(tail))}`);
343
+ const logs = (result.logs || []).map((entry) => typeof entry === 'string'
344
+ ? { level: 'INFO', message: entry, timestamp: '' }
345
+ : entry);
346
+ return { logs, lines: logs.map((entry) => entry.message) };
347
+ }
348
+ /**
349
+ * Stop a running training job.
350
+ *
351
+ * By default, intermediate data (checkpoints, logs) is preserved.
352
+ * Pass `keepData: false` to clean up all job artifacts.
353
+ *
354
+ * @param id - Training job ID.
355
+ * @param keepData - Whether to keep checkpoints and logs. Defaults to true.
356
+ *
357
+ * @example
358
+ * ```ts
359
+ * await client.training.stop('job_abc123');
360
+ * ```
361
+ */
362
+ async stop(id, keepData = true) {
363
+ return this._http.fetchPost(`/api/training/jobs/${encodeURIComponent(id)}/stop`, { keep_data: keepData });
364
+ }
365
+ /**
366
+ * Resume a stopped training job from its last checkpoint.
367
+ *
368
+ * @param id - Training job ID.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * await client.training.resume('job_abc123');
373
+ * ```
374
+ */
375
+ async resume(id, idempotencyKey) {
376
+ const result = await this._http.fetchPost(`/api/training/jobs/${encodeURIComponent(id)}/resume`, undefined, { 'Idempotency-Key': trainingIdempotencyKey(idempotencyKey) });
377
+ const resumedId = result.id || result.job_id;
378
+ if (!resumedId)
379
+ throw new Error('BiOS: resume response did not include job_id');
380
+ return { ...result, id: resumedId };
381
+ }
382
+ /**
383
+ * Delete a specific checkpoint from a training job.
384
+ *
385
+ * @param jobId - Training job ID.
386
+ * @param checkpointId - Checkpoint ID to delete.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * await client.training.deleteCheckpoint('job_abc123', 'cp_xyz789');
391
+ * ```
392
+ */
393
+ async getEvals(id) {
394
+ return this._http.fetchGet(`/api/training/jobs/${encodeURIComponent(id)}/evals`);
395
+ }
396
+ async deleteCheckpoint(jobId, checkpointId) {
397
+ await this._http.fetchDelete(`/api/training/jobs/${encodeURIComponent(jobId)}/checkpoints/${encodeURIComponent(checkpointId)}`);
398
+ }
399
+ }