trainfabric 0.1.29 → 0.1.31

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.cjs +156 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,9 +26,11 @@ Secrets are stored in macOS Keychain when available. Other platforms use an encr
26
26
 
27
27
  ```bash
28
28
  trainfabric projects:list
29
+ trainfabric projects:create --name <name>
30
+ trainfabric projects:delete <projectId>
29
31
  trainfabric datasets:upload ./train.jsonl --project <projectId>
30
32
  trainfabric runs:quote --summary --project <projectId> --dataset <datasetId> --model llama-3-8b
31
- trainfabric runs:create --yes --quote <quoteId> --project <projectId> --dataset <datasetId> --model llama-3-8b
33
+ trainfabric runs:create --yes --quote <quoteId>
32
34
  trainfabric runs:watch <runId> --timeout 30m
33
35
  trainfabric runs:cost-breakdown <runId> --summary
34
36
  ```
package/dist/index.cjs CHANGED
@@ -8077,6 +8077,15 @@ var ResourceClient = class {
8077
8077
  };
8078
8078
 
8079
8079
  // ../sdk/dist/client/admin.js
8080
+ var apiKeyIdPattern = /^key_[A-Za-z0-9_-]+$/;
8081
+ var serviceAccountIdPattern = /^svc_[A-Za-z0-9_-]+$/;
8082
+ function normalizeRequiredId(value, pattern, label) {
8083
+ const id = String(value ?? "").trim();
8084
+ if (!pattern.test(id)) {
8085
+ throw new Error(`${label} is invalid.`);
8086
+ }
8087
+ return encodeURIComponent(id);
8088
+ }
8080
8089
  var ApiKeysClient = class extends ResourceClient {
8081
8090
  create(input) {
8082
8091
  apiKeyCreateSchema.parse(input);
@@ -8086,7 +8095,7 @@ var ApiKeysClient = class extends ResourceClient {
8086
8095
  return this.requestGet("/v1/api-keys");
8087
8096
  }
8088
8097
  revoke(apiKeyId) {
8089
- return this.requestDelete(`/v1/api-keys/${apiKeyId}`);
8098
+ return this.requestDelete(`/v1/api-keys/${normalizeRequiredId(apiKeyId, apiKeyIdPattern, "API key ID")}`);
8090
8099
  }
8091
8100
  };
8092
8101
  var ServiceAccountsClient = class extends ResourceClient {
@@ -8098,7 +8107,7 @@ var ServiceAccountsClient = class extends ResourceClient {
8098
8107
  return this.requestGet("/v1/service-accounts");
8099
8108
  }
8100
8109
  revoke(serviceAccountId) {
8101
- return this.requestDelete(`/v1/service-accounts/${serviceAccountId}`);
8110
+ return this.requestDelete(`/v1/service-accounts/${normalizeRequiredId(serviceAccountId, serviceAccountIdPattern, "Service account ID")}`);
8102
8111
  }
8103
8112
  };
8104
8113
 
@@ -8164,6 +8173,14 @@ var ClusterClient = class extends ResourceClient {
8164
8173
  // ../sdk/dist/client/datasets.js
8165
8174
  var import_promises = __toESM(require("node:fs/promises"), 1);
8166
8175
  var import_node_path = __toESM(require("node:path"), 1);
8176
+ var datasetIdPattern = /^ds_[A-Za-z0-9_-]+$/;
8177
+ function encodeDatasetId(datasetId) {
8178
+ const id = String(datasetId ?? "").trim();
8179
+ if (!datasetIdPattern.test(id)) {
8180
+ throw new Error("Dataset ID is invalid.");
8181
+ }
8182
+ return encodeURIComponent(id);
8183
+ }
8167
8184
  var DatasetsClient = class extends ResourceClient {
8168
8185
  async create(input) {
8169
8186
  const validation = await this.validate({
@@ -8178,7 +8195,7 @@ var DatasetsClient = class extends ResourceClient {
8178
8195
  throw new Error("projectId is required.");
8179
8196
  }
8180
8197
  const fileName = import_node_path.default.basename(input.path);
8181
- const fileBuffer = await import_promises.default.readFile(input.path);
8198
+ const fileBuffer = await readDatasetFile(input.path);
8182
8199
  const uploadSessionInput = uploadSessionCreateSchema.parse({
8183
8200
  projectId,
8184
8201
  fileName,
@@ -8204,21 +8221,46 @@ var DatasetsClient = class extends ResourceClient {
8204
8221
  if (input.format !== "chat_jsonl") {
8205
8222
  throw new Error("Only chat_jsonl datasets are supported.");
8206
8223
  }
8207
- return validateChatJsonl(await import_promises.default.readFile(input.path, "utf8"));
8224
+ return validateChatJsonl(await readDatasetFile(input.path, "utf8"));
8208
8225
  }
8209
8226
  get(datasetId) {
8210
- return this.requestGet(`/v1/datasets/${datasetId}`);
8227
+ return this.requestGet(`/v1/datasets/${encodeDatasetId(datasetId)}`);
8211
8228
  }
8212
8229
  list(projectId) {
8213
8230
  const effectiveProjectId = projectId ?? this.parent.projectId;
8214
- return this.requestGet(`/v1/datasets${effectiveProjectId ? `?projectId=${effectiveProjectId}` : ""}`);
8231
+ const params = new URLSearchParams();
8232
+ if (effectiveProjectId) {
8233
+ params.set("projectId", effectiveProjectId);
8234
+ }
8235
+ const query = params.toString();
8236
+ return this.requestGet(`/v1/datasets${query ? `?${query}` : ""}`);
8215
8237
  }
8216
8238
  delete(datasetId) {
8217
- return this.requestDelete(`/v1/datasets/${datasetId}`);
8239
+ return this.requestDelete(`/v1/datasets/${encodeDatasetId(datasetId)}`);
8218
8240
  }
8219
8241
  };
8242
+ async function readDatasetFile(pathname, encoding) {
8243
+ let stats;
8244
+ try {
8245
+ stats = await import_promises.default.stat(pathname);
8246
+ } catch {
8247
+ throw new Error(`Dataset file not found: ${pathname}`);
8248
+ }
8249
+ if (!stats.isFile()) {
8250
+ throw new Error(`Dataset path must be a file: ${pathname}`);
8251
+ }
8252
+ return encoding ? import_promises.default.readFile(pathname, encoding) : import_promises.default.readFile(pathname);
8253
+ }
8220
8254
 
8221
8255
  // ../sdk/dist/client/deployments.js
8256
+ var deploymentIdPattern = /^dep_[A-Za-z0-9_-]+$/;
8257
+ function encodeDeploymentId(deploymentId) {
8258
+ const id = String(deploymentId ?? "").trim();
8259
+ if (!deploymentIdPattern.test(id)) {
8260
+ throw new Error("Deployment ID is invalid.");
8261
+ }
8262
+ return encodeURIComponent(id);
8263
+ }
8222
8264
  var DeploymentsClient = class extends ResourceClient {
8223
8265
  list() {
8224
8266
  return this.requestGet("/v1/deployments");
@@ -8227,23 +8269,31 @@ var DeploymentsClient = class extends ResourceClient {
8227
8269
  return this.requestPost("/v1/deployments", input);
8228
8270
  }
8229
8271
  get(id) {
8230
- return this.requestGet(`/v1/deployments/${id}`);
8272
+ return this.requestGet(`/v1/deployments/${encodeDeploymentId(id)}`);
8231
8273
  }
8232
8274
  delete(id) {
8233
- return this.requestDelete(`/v1/deployments/${id}`);
8275
+ return this.requestDelete(`/v1/deployments/${encodeDeploymentId(id)}`);
8234
8276
  }
8235
8277
  };
8236
8278
 
8237
8279
  // ../sdk/dist/client/models.js
8280
+ var modelIdPattern = /^mdl_[A-Za-z0-9_-]+$/;
8281
+ function normalizeModelId(modelId) {
8282
+ const normalized = String(modelId ?? "").trim();
8283
+ if (!modelIdPattern.test(normalized)) {
8284
+ throw new Error("Model ID is invalid.");
8285
+ }
8286
+ return encodeURIComponent(normalized);
8287
+ }
8238
8288
  var ModelsClient = class extends ResourceClient {
8239
8289
  list() {
8240
8290
  return this.requestGet("/v1/models");
8241
8291
  }
8242
8292
  get(modelId) {
8243
- return this.requestGet(`/v1/models/${modelId}`);
8293
+ return this.requestGet(`/v1/models/${normalizeModelId(modelId)}`);
8244
8294
  }
8245
8295
  export(modelId) {
8246
- return this.requestPost(`/v1/models/${modelId}/export`);
8296
+ return this.requestPost(`/v1/models/${normalizeModelId(modelId)}/export`);
8247
8297
  }
8248
8298
  };
8249
8299
 
@@ -8255,12 +8305,20 @@ var ProjectsClient = class extends ResourceClient {
8255
8305
  organizationId: input.organizationId ?? this.parent.orgId
8256
8306
  });
8257
8307
  }
8308
+ delete(projectId) {
8309
+ return this.requestDelete(`/v1/projects/${projectId}`);
8310
+ }
8258
8311
  get(projectId) {
8259
8312
  return this.requestGet(`/v1/projects/${projectId}`);
8260
8313
  }
8261
8314
  list(organizationId) {
8262
8315
  const suffix = organizationId ?? this.parent.orgId;
8263
- return this.requestGet(`/v1/projects${suffix ? `?organizationId=${suffix}` : ""}`);
8316
+ const params = new URLSearchParams();
8317
+ if (suffix) {
8318
+ params.set("organizationId", suffix);
8319
+ }
8320
+ const query = params.toString();
8321
+ return this.requestGet(`/v1/projects${query ? `?${query}` : ""}`);
8264
8322
  }
8265
8323
  };
8266
8324
  var OrganizationsClient = class extends ResourceClient {
@@ -8420,15 +8478,6 @@ var RunHandle = class {
8420
8478
  this.snapshot = detail.run;
8421
8479
  if (["completed", "failed", "canceled", "terminated"].includes(detail.run.status)) {
8422
8480
  this.close();
8423
- if (detail.run.status === "failed") {
8424
- throw new Error(detail.run.failureReason ?? "Run failed.");
8425
- }
8426
- if (detail.run.status === "canceled") {
8427
- throw new Error("Run was canceled.");
8428
- }
8429
- if (detail.run.status === "terminated") {
8430
- throw new Error("Run was terminated.");
8431
- }
8432
8481
  return detail;
8433
8482
  }
8434
8483
  if (timeoutMs !== void 0 && Date.now() - startedAt > timeoutMs) {
@@ -8514,6 +8563,14 @@ var RunHandle = class {
8514
8563
  };
8515
8564
 
8516
8565
  // ../sdk/dist/client/runs.js
8566
+ var runIdPattern = /^run_[A-Za-z0-9_-]+$/;
8567
+ function encodeRunId(runId) {
8568
+ const id = String(runId ?? "").trim();
8569
+ if (!runIdPattern.test(id)) {
8570
+ throw new Error("Run ID is invalid.");
8571
+ }
8572
+ return encodeURIComponent(id);
8573
+ }
8517
8574
  var RunsClient = class extends ResourceClient {
8518
8575
  describeEmptyQuoteFailure(input) {
8519
8576
  const compute = input.compute && input.compute !== "auto" && input.compute !== "cpu-sim" ? input.compute : void 0;
@@ -8611,47 +8668,52 @@ var RunsClient = class extends ResourceClient {
8611
8668
  mode: selectedModeQuote.mode
8612
8669
  };
8613
8670
  }
8614
- const parsed = runCreateSchema.parse(payload);
8671
+ const parsed = input.pricingQuoteId ? payload : runCreateSchema.parse(payload);
8615
8672
  const run = await this.requestPost("/v1/runs", parsed);
8616
8673
  return new RunHandle(this.parent, run);
8617
8674
  }
8618
8675
  get(runId) {
8619
- return this.requestGet(`/v1/runs/${runId}`);
8676
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}`);
8620
8677
  }
8621
8678
  list(projectId) {
8622
8679
  const effectiveProjectId = projectId ?? this.parent.projectId;
8623
- return this.requestGet(`/v1/runs${effectiveProjectId ? `?projectId=${effectiveProjectId}` : ""}`);
8680
+ const params = new URLSearchParams();
8681
+ if (effectiveProjectId) {
8682
+ params.set("projectId", effectiveProjectId);
8683
+ }
8684
+ const query = params.toString();
8685
+ return this.requestGet(`/v1/runs${query ? `?${query}` : ""}`);
8624
8686
  }
8625
8687
  cancel(runId) {
8626
- return this.requestPost(`/v1/runs/${runId}/cancel`);
8688
+ return this.requestPost(`/v1/runs/${encodeRunId(runId)}/cancel`);
8627
8689
  }
8628
8690
  resume(runId) {
8629
- return this.requestPost(`/v1/runs/${runId}/resume`);
8691
+ return this.requestPost(`/v1/runs/${encodeRunId(runId)}/resume`);
8630
8692
  }
8631
8693
  terminate(runId) {
8632
- return this.requestPost(`/v1/runs/${runId}/terminate`);
8694
+ return this.requestPost(`/v1/runs/${encodeRunId(runId)}/terminate`);
8633
8695
  }
8634
8696
  logs(runId) {
8635
- return this.requestGet(`/v1/runs/${runId}/logs`);
8697
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/logs`);
8636
8698
  }
8637
8699
  async watch(runId) {
8638
8700
  const detail = await this.get(runId);
8639
8701
  return new RunHandle(this.parent, detail.run);
8640
8702
  }
8641
8703
  allocation(runId) {
8642
- return this.requestGet(`/v1/runs/${runId}/allocation`);
8704
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/allocation`);
8643
8705
  }
8644
8706
  usage(runId) {
8645
- return this.requestGet(`/v1/runs/${runId}/usage`);
8707
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/usage`);
8646
8708
  }
8647
8709
  costBreakdown(runId) {
8648
- return this.requestGet(`/v1/runs/${runId}/cost-breakdown`);
8710
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/cost-breakdown`);
8649
8711
  }
8650
8712
  configuration(runId) {
8651
- return this.requestGet(`/v1/runs/${runId}/configuration`);
8713
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/configuration`);
8652
8714
  }
8653
8715
  explanation(runId) {
8654
- return this.requestGet(`/v1/runs/${runId}/explanation`);
8716
+ return this.requestGet(`/v1/runs/${encodeRunId(runId)}/explanation`);
8655
8717
  }
8656
8718
  async wait(runId, options) {
8657
8719
  const handle = await this.watch(runId);
@@ -8666,7 +8728,12 @@ var RuntimeClient = class extends ResourceClient {
8666
8728
  projectId: input.projectId ?? this.parent.projectId,
8667
8729
  source: input.source
8668
8730
  });
8669
- return this.requestPost("/v1/runtime/detect", payload);
8731
+ return this.requestPost("/v1/runtime/detect", payload).catch((error) => {
8732
+ if (input.source.kind === "git" && error instanceof Error && /Request failed with status 404/.test(error.message)) {
8733
+ throw new Error("Git repository could not be resolved. Check the repository URL, branch, and access permissions.");
8734
+ }
8735
+ throw error;
8736
+ });
8670
8737
  }
8671
8738
  build(input) {
8672
8739
  const payload = runtimeBuildCreateSchema.parse({
@@ -8828,6 +8895,7 @@ var idPatterns = {
8828
8895
  org: /^org_[A-Za-z0-9_-]+$/,
8829
8896
  pool: /^pool_[A-Za-z0-9_-]+$/,
8830
8897
  project: /^proj_[A-Za-z0-9_-]+$/,
8898
+ quote: /^quote_[A-Za-z0-9_-]+$/,
8831
8899
  run: /^run_[A-Za-z0-9_-]+$/,
8832
8900
  serviceAccount: /^svc_[A-Za-z0-9_-]+$/,
8833
8901
  supplier: /^[A-Za-z][A-Za-z0-9_-]*$/
@@ -9139,7 +9207,7 @@ function buildComputeSpec(options) {
9139
9207
 
9140
9208
  // src/index.ts
9141
9209
  var DEFAULT_TRAINFABRIC_API_URL2 = "https://api.trainfabric.com";
9142
- var CLI_VERSION = "0.1.29";
9210
+ var CLI_VERSION = "0.1.31";
9143
9211
  var CONFIG_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".trainfabric");
9144
9212
  var CONFIG_PATH = import_node_path3.default.join(CONFIG_DIR, "config.json");
9145
9213
  var FALLBACK_SECRET_PATH = import_node_path3.default.join(CONFIG_DIR, "session.enc");
@@ -9486,23 +9554,43 @@ function requireProjectId(options, config) {
9486
9554
  }
9487
9555
  function buildRunInput(options, config = loadConfig()) {
9488
9556
  const sourceOptions = buildSourceOptions(options);
9489
- return {
9490
- projectId: requireProjectId(options, config),
9557
+ const quoteId = options.quote === void 0 ? void 0 : normalizeId(options.quote, "quote", "Pricing quote ID");
9558
+ if (!quoteId && !options.dataset) {
9559
+ throw new Error("Dataset is required unless --quote is provided.");
9560
+ }
9561
+ if (!quoteId && !options.model) {
9562
+ throw new Error("Model is required unless --quote is provided.");
9563
+ }
9564
+ const projectId = normalizeOptionalId(options.project ?? config.projectId, "project", "Project ID");
9565
+ if (!quoteId && !projectId) {
9566
+ throw new Error("Project is required. Pass --project <projectId> or run `trainfabric config:set-project <projectId>`.");
9567
+ }
9568
+ const input = {
9491
9569
  task: "sft",
9492
9570
  method: "lora",
9493
- baseModel: normalizeBaseModel(options.model),
9494
- datasetId: normalizeId(options.dataset, "dataset", "Dataset ID"),
9571
+ pricingQuoteId: quoteId,
9495
9572
  evalDatasetId: normalizeOptionalId(options.eval, "dataset", "Eval dataset ID"),
9496
- pricingQuoteId: options.quote,
9497
9573
  ...sourceOptions,
9498
- mode: normalizeTrainingMode(options.mode),
9499
- compute: buildComputeSpec(options),
9500
- hyperparameters: {
9574
+ mode: normalizeTrainingMode(options.mode)
9575
+ };
9576
+ if (projectId) {
9577
+ input.projectId = projectId;
9578
+ }
9579
+ if (options.model) {
9580
+ input.baseModel = normalizeBaseModel(options.model);
9581
+ }
9582
+ if (options.dataset) {
9583
+ input.datasetId = normalizeId(options.dataset, "dataset", "Dataset ID");
9584
+ }
9585
+ if (!quoteId) {
9586
+ input.compute = buildComputeSpec(options);
9587
+ input.hyperparameters = {
9501
9588
  epochs: normalizeEpochs(options.epochs),
9502
9589
  lr: normalizeLearningRate(options.lr),
9503
9590
  batchSize: "auto"
9504
- }
9505
- };
9591
+ };
9592
+ }
9593
+ return input;
9506
9594
  }
9507
9595
  function assertQuoteOptions(bundle, options) {
9508
9596
  if (bundle.quotes.length > 0) {
@@ -9761,6 +9849,11 @@ program2.command("projects:create").requiredOption("--name <name>").option("--or
9761
9849
  })
9762
9850
  );
9763
9851
  });
9852
+ program2.command("projects:delete").argument("<projectId>").description("Delete an empty project").action(async (projectId) => {
9853
+ const normalizedProjectId = normalizeId(projectId, "project", "Project ID");
9854
+ await createClient(loadConfig()).projects.delete(normalizedProjectId);
9855
+ printJson({ deleted: true, id: normalizedProjectId });
9856
+ });
9764
9857
  program2.command("datasets:validate").argument("<file>").description("Validate a local dataset file").action(async (file) => {
9765
9858
  assertReadableFile(file, "Dataset path");
9766
9859
  const validation = await createClient(loadConfig()).datasets.validate({
@@ -9796,7 +9889,7 @@ program2.command("datasets:upload").argument("<file>").option("--project <projec
9796
9889
  });
9797
9890
  printJson(dataset);
9798
9891
  });
9799
- program2.command("runs:create").option("--project <projectId>").requiredOption("--dataset <datasetId>").requiredOption("--model <baseModel>").option("--eval <evalDatasetId>").option("--epochs <epochs>", "number of epochs", "3").option("--lr <lr>", "learning rate", "0.0002").option("--gpus <gpuCount>", "gpu count", "1").option("--nodes <nodeCount>", "node count", "1").option("--accelerator <acceleratorClass>", "optional hard accelerator constraint (for example: a10g, a100, h100)").option("--min-memory <gigabytes>", "minimum GPU memory in GB").option("--precision <precision>", "fp16_bf16, fp8, or fp32").option("--interconnect <interconnect>", "pcie or nvlink").option("--mode <mode>", "efficient, balanced, or power", "balanced").option("--quote <pricingQuoteId>", "launch the exact accepted quote id from runs:quote").option("--repo <path>", "local repo path for runtime autodetect").option("--git <url>", "git repo URL for runtime metadata").option("--branch <branch>", "git branch for runtime metadata").option("--yes", "confirm that you reviewed pricing with runs:quote --summary and accept fluctuating realized usage").description("Create a training run").action(async (options) => {
9892
+ program2.command("runs:create").option("--project <projectId>").option("--dataset <datasetId>").option("--model <baseModel>").option("--eval <evalDatasetId>").option("--epochs <epochs>", "number of epochs", "3").option("--lr <lr>", "learning rate", "0.0002").option("--gpus <gpuCount>", "gpu count", "1").option("--nodes <nodeCount>", "node count", "1").option("--accelerator <acceleratorClass>", "optional hard accelerator constraint (for example: a10g, a100, h100)").option("--min-memory <gigabytes>", "minimum GPU memory in GB").option("--precision <precision>", "fp16_bf16, fp8, or fp32").option("--interconnect <interconnect>", "pcie or nvlink").option("--mode <mode>", "efficient, balanced, or power").option("--quote <pricingQuoteId>", "launch the exact accepted quote id from runs:quote").option("--repo <path>", "local repo path for runtime autodetect").option("--git <url>", "git repo URL for runtime metadata").option("--branch <branch>", "git branch for runtime metadata").option("--yes", "confirm that you reviewed pricing with runs:quote --summary and accept fluctuating realized usage").description("Create a training run").action(async (options) => {
9800
9893
  const config = loadConfig();
9801
9894
  const runInput = buildRunInput(options, config);
9802
9895
  if (!options.yes) {
@@ -9812,10 +9905,23 @@ program2.command("runtime:detect").option("--project <projectId>").option("--rep
9812
9905
  throw new Error("Provide --repo or --git.");
9813
9906
  }
9814
9907
  const config = loadConfig();
9815
- const detection = await createClient(config).runtime.detect({
9816
- projectId: requireProjectId(options, config),
9817
- source: sourceOptions.source
9818
- });
9908
+ let detection;
9909
+ try {
9910
+ detection = await createClient(config).runtime.detect({
9911
+ projectId: requireProjectId(options, config),
9912
+ source: sourceOptions.source
9913
+ });
9914
+ } catch (error) {
9915
+ if (sourceOptions.source.kind === "git" && error instanceof Error && /Request failed with status 404/.test(error.message)) {
9916
+ throw new Error("Git repository could not be resolved. Check the repository URL, branch, and access permissions.");
9917
+ }
9918
+ throw error;
9919
+ }
9920
+ if (sourceOptions.source.kind === "git" && detection.status !== "detected") {
9921
+ throw new Error(
9922
+ `Runtime source is ${detection.status}. Git repository could not be resolved into a supported runtime source.`
9923
+ );
9924
+ }
9819
9925
  printJson(redactInlineSource(detection));
9820
9926
  });
9821
9927
  program2.command("runtime:build").option("--project <projectId>").requiredOption("--detection <detectionId>").description("Build or reuse a cached runtime image from a runtime detection").action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trainfabric",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Trainfabric CLI for launching GPU training jobs on the hosted Trainfabric backend.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",