@camstack/addon-training 0.1.5 → 0.1.7

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 (44) hide show
  1. package/dist/MaskShapeCanvas-Kfg9dl6B.mjs +686 -0
  2. package/dist/MotionZonesSettings-DKtYdHUO.mjs +292 -0
  3. package/dist/{PrivacyMaskSettings-BwYjIkQR.mjs → PrivacyMaskSettings-BvLUVz86.mjs} +45 -70
  4. package/dist/ReactKonva-DGuYUN0L.mjs +5416 -0
  5. package/dist/{SceneMonitorEditor-BBfow3Lm.mjs → SceneMonitorEditor-rFYGkGHV.mjs} +344 -344
  6. package/dist/_stub.js +25672 -10252
  7. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-CGhkyuU-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-B3xXUcaU.mjs} +3 -3
  8. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C7dks3FG.mjs +26 -0
  9. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-DpjirGmB.mjs +89 -0
  10. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-CFCVuRAs.mjs +58 -0
  11. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-BkvDudqY.mjs +62 -0
  12. package/dist/{_virtual_mf___mfe_internal__addon_training_page__loadShare__react__loadShare__.js-BT_wL8s3.mjs → _virtual_mf___mfe_internal__addon_training_page__loadShare__react__loadShare__.js-DeDbF7NI.mjs} +1 -1
  13. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DnrbBvrw.mjs +34 -0
  14. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom__loadShare__.js-DAZkHxxU.mjs +45 -0
  15. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-CHqN9NLt.mjs +34 -0
  16. package/dist/addon-training.css +1 -1
  17. package/dist/{dist-BiSFqfpy.js → dist-DVDBSrjw.js} +282 -147
  18. package/dist/{dist-B_79m03G.mjs → dist-Phot2UwD.mjs} +283 -142
  19. package/dist/grid-2x2-Bl3xw1w7.mjs +21 -0
  20. package/dist/{hostInit-BEPknhsh.mjs → hostInit-BAGO4XYA.mjs} +2 -2
  21. package/dist/index.js +100 -39
  22. package/dist/index.mjs +99 -38
  23. package/dist/lib-CVGFTwe0.mjs +7770 -0
  24. package/dist/orchestrator/training.addon.js +376 -222
  25. package/dist/orchestrator/training.addon.mjs +376 -222
  26. package/dist/{player-overlays-wE5IcDiI.mjs → player-overlays-BT-6fK9N.mjs} +29 -10
  27. package/dist/providers/modal/training-modal.addon.js +361 -155
  28. package/dist/providers/modal/training-modal.addon.mjs +364 -158
  29. package/dist/remoteEntry.js +1 -1
  30. package/dist/{responsive-D17kMsm1.mjs → responsive-Dy5RAD1s.mjs} +42 -38
  31. package/dist/{scene-monitor-copy-LjlvhSRM.mjs → scene-monitor-copy-7HImRbA5.mjs} +4 -4
  32. package/dist/{square-JGDHhLlY.mjs → square-DoPBhbZN.mjs} +29 -4
  33. package/dist/{trash-2-zeFnLJvf.mjs → trash-2-BwunNgLf.mjs} +1 -1
  34. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-C0RB_rkV.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-gU49KvnZ.mjs} +399 -390
  35. package/package.json +1 -1
  36. package/dist/MaskShapeCanvas-BokARvOj.mjs +0 -13806
  37. package/dist/MotionZonesSettings-aj085Nrn.mjs +0 -310
  38. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BkKuzhOJ.mjs +0 -26
  39. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-CaB1jnkt.mjs +0 -25
  40. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-B3rryujf.mjs +0 -25
  41. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-nL4MGFRP.mjs +0 -26
  42. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DDBEvLdo.mjs +0 -26
  43. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom__loadShare__.js-i_pnGDxd.mjs +0 -25
  44. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-gTIQrhKs.mjs +0 -25
@@ -1,11 +1,11 @@
1
- import { d as boolean, f as number, h as string, l as trainingProviderCapability, m as record, p as object, u as BaseAddon } from "../../dist-B_79m03G.mjs";
1
+ import { c as trainingProviderCapability, d as number, f as object, l as BaseAddon, m as string, p as record, u as boolean } from "../../dist-Phot2UwD.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import path, { dirname, posix } from "node:path";
4
- import { createHash, createPrivateKey, createPublicKey, createSign, randomBytes } from "node:crypto";
4
+ import { createHash, createPrivateKey, createPublicKey, createSign, randomBytes, randomUUID } from "node:crypto";
5
5
  import { createWriteStream, readFileSync } from "node:fs";
6
6
  import { mkdir, open, rename, rm, unlink } from "node:fs/promises";
7
7
  import { createRequire as createRequire$1 } from "module";
8
- import { randomFillSync, randomUUID } from "crypto";
8
+ import { randomFillSync, randomUUID as randomUUID$1 } from "crypto";
9
9
  import { setTimeout as setTimeout$1 } from "timers/promises";
10
10
  import { warn } from "node:console";
11
11
  import { homedir } from "node:os";
@@ -39,6 +39,124 @@ var __copyProps = (to, from, except, desc) => {
39
39
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
40
40
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
41
41
  //#endregion
42
+ //#region src/providers/modal/modal-entries.ts
43
+ /**
44
+ * Modal provider entries — one credentials + defaults row per operator
45
+ * account. Pure helpers so the store is unit-tested without BaseAddon.
46
+ *
47
+ * A pre-multi-entry config (flat tokenId/tokenSecret on the addon) migrates
48
+ * to a single entry named {@link DEFAULT_MODAL_ENTRY_NAME}.
49
+ */
50
+ var MODAL_TYPE_ID = "modal";
51
+ var DEFAULT_MODAL_ENTRY_NAME = "Default";
52
+ var MODAL_ICON_ASSET = "assets/training-modal/icon.svg";
53
+ var DEFAULT_GPU = "L4";
54
+ var DEFAULT_COST = 5;
55
+ var DEFAULT_HOURS = 4;
56
+ var DEFAULT_GRACE = 24;
57
+ function legacyConfigured(config) {
58
+ return (config.tokenId ?? "").trim() !== "" || (config.tokenSecret ?? "").trim() !== "";
59
+ }
60
+ /** Entries the operator sees — synthesises Default from legacy flat fields. */
61
+ function effectiveModalEntries(config) {
62
+ if (config.entries.length > 0) return config.entries;
63
+ if (!legacyConfigured(config)) return [];
64
+ return [{
65
+ id: "legacy-default",
66
+ name: DEFAULT_MODAL_ENTRY_NAME,
67
+ tokenId: config.tokenId ?? "",
68
+ tokenSecret: config.tokenSecret ?? "",
69
+ environment: config.environment ?? "",
70
+ gpuType: config.gpuType ?? DEFAULT_GPU,
71
+ region: config.region ?? "",
72
+ costCapUsd: config.costCapUsd ?? DEFAULT_COST,
73
+ maxRuntimeHours: config.maxRuntimeHours ?? DEFAULT_HOURS,
74
+ failedDatasetGraceHours: config.failedDatasetGraceHours ?? DEFAULT_GRACE
75
+ }];
76
+ }
77
+ function findModalEntry(config, entryId) {
78
+ return effectiveModalEntries(config).find((e) => e.id === entryId) ?? null;
79
+ }
80
+ /** Persist a Default entry from legacy flat fields; clear the flat secrets. */
81
+ function migrateLegacyModalConfig(config, newId) {
82
+ if (config.entries.length > 0) return config;
83
+ if (!legacyConfigured(config)) return config;
84
+ const entry = {
85
+ id: newId(),
86
+ name: DEFAULT_MODAL_ENTRY_NAME,
87
+ tokenId: config.tokenId ?? "",
88
+ tokenSecret: config.tokenSecret ?? "",
89
+ environment: config.environment ?? "",
90
+ gpuType: config.gpuType ?? DEFAULT_GPU,
91
+ region: config.region ?? "",
92
+ costCapUsd: config.costCapUsd ?? DEFAULT_COST,
93
+ maxRuntimeHours: config.maxRuntimeHours ?? DEFAULT_HOURS,
94
+ failedDatasetGraceHours: config.failedDatasetGraceHours ?? DEFAULT_GRACE
95
+ };
96
+ return {
97
+ entries: [entry],
98
+ tokenId: "",
99
+ tokenSecret: "",
100
+ environment: "",
101
+ gpuType: entry.gpuType,
102
+ region: entry.region,
103
+ costCapUsd: entry.costCapUsd,
104
+ maxRuntimeHours: entry.maxRuntimeHours,
105
+ failedDatasetGraceHours: entry.failedDatasetGraceHours
106
+ };
107
+ }
108
+ function createModalEntry(config, input, newId) {
109
+ if (input.typeId !== "modal") return {
110
+ ok: false,
111
+ reason: "unknown-type"
112
+ };
113
+ const base = migrateLegacyModalConfig(config, newId);
114
+ const entry = {
115
+ id: newId(),
116
+ name: input.name.trim() === "" ? DEFAULT_MODAL_ENTRY_NAME : input.name.trim(),
117
+ tokenId: "",
118
+ tokenSecret: "",
119
+ environment: "",
120
+ gpuType: base.gpuType ?? DEFAULT_GPU,
121
+ region: base.region ?? "",
122
+ costCapUsd: base.costCapUsd ?? DEFAULT_COST,
123
+ maxRuntimeHours: base.maxRuntimeHours ?? DEFAULT_HOURS,
124
+ failedDatasetGraceHours: base.failedDatasetGraceHours ?? DEFAULT_GRACE
125
+ };
126
+ return {
127
+ ok: true,
128
+ entryId: entry.id,
129
+ config: {
130
+ ...base,
131
+ entries: [...base.entries, entry],
132
+ tokenId: "",
133
+ tokenSecret: ""
134
+ }
135
+ };
136
+ }
137
+ function deleteModalEntry(config, entryId) {
138
+ const current = effectiveModalEntries(config);
139
+ if (!current.some((e) => e.id === entryId)) return {
140
+ ok: false,
141
+ reason: "unknown-entry"
142
+ };
143
+ return {
144
+ ok: true,
145
+ entryId,
146
+ config: {
147
+ entries: current.filter((e) => e.id !== entryId),
148
+ tokenId: "",
149
+ tokenSecret: "",
150
+ environment: config.environment ?? "",
151
+ gpuType: config.gpuType ?? DEFAULT_GPU,
152
+ region: config.region ?? "",
153
+ costCapUsd: config.costCapUsd ?? DEFAULT_COST,
154
+ maxRuntimeHours: config.maxRuntimeHours ?? DEFAULT_HOURS,
155
+ failedDatasetGraceHours: config.failedDatasetGraceHours ?? DEFAULT_GRACE
156
+ }
157
+ };
158
+ }
159
+ //#endregion
42
160
  //#region src/providers/modal/dockerfile.ts
43
161
  var ALLOWED = new Set([
44
162
  "ARG",
@@ -234,30 +352,32 @@ var ModalTrainingProvider = class {
234
352
  constructor(deps) {
235
353
  this.deps = deps;
236
354
  }
237
- describe = async () => {
238
- const s = this.deps.settings();
239
- return [{
240
- addonId: this.deps.addonId,
241
- kind: "modal",
242
- label: "Modal",
243
- gpuTypes: MODAL_GPUS,
244
- regions: MODAL_REGIONS,
245
- maxDatasetBytes: MODAL_MAX_DATASET_BYTES,
246
- maxRuntimeSec: MODAL_MAX_RUNTIME_SEC,
247
- freeTierNote: "The Starter plan includes $30 of credit every month; Modal bills per second.",
248
- dataResidencyNote: "Modal keeps the frames on a Volume of your workspace for the length of the job; the training container sees them only while it runs.",
249
- configured: this.configured(),
250
- defaults: {
251
- gpuType: s.gpuType,
252
- region: s.region,
253
- costCapUsd: s.costCapUsd,
254
- maxRuntimeSec: Math.round(s.maxRuntimeHours * 3600),
255
- failedDatasetGraceHours: s.failedDatasetGraceHours
256
- }
257
- }];
355
+ describe = async () => this.deps.listEntries().map((entry) => this.descriptorOf(entry));
356
+ describeTypes = async () => [{
357
+ typeId: MODAL_TYPE_ID,
358
+ label: "Modal",
359
+ iconAsset: MODAL_ICON_ASSET,
360
+ addonId: this.deps.addonId
361
+ }];
362
+ listEntries = async () => this.deps.listEntries().map((entry) => ({
363
+ entryId: entry.id,
364
+ typeId: MODAL_TYPE_ID,
365
+ name: entry.name,
366
+ addonId: this.deps.addonId,
367
+ configured: this.entryConfigured(entry)
368
+ }));
369
+ createEntry = async (input) => this.deps.createEntry(input);
370
+ deleteEntry = async (input) => {
371
+ await this.deps.deleteEntry(input.entryId);
372
+ return { ok: true };
258
373
  };
259
- validateCredentials = async () => {
260
- const gateway = this.deps.gateway();
374
+ validateCredentials = async (input) => {
375
+ if (this.deps.entry(input.entryId) === null) return {
376
+ ok: false,
377
+ reason: "missing",
378
+ detail: "unknown entry"
379
+ };
380
+ const gateway = this.deps.gateway(input.entryId);
261
381
  if (gateway === null) return {
262
382
  ok: false,
263
383
  reason: "missing",
@@ -291,7 +411,7 @@ var ModalTrainingProvider = class {
291
411
  };
292
412
  };
293
413
  uploadDataset = async (input) => {
294
- const gateway = this.requireGateway();
414
+ const gateway = this.requireGateway(input.entryId);
295
415
  const opened = await this.deps.peer.open(input.ticket);
296
416
  if (opened.kind === "refused") throw new Error(`dataset offer refused: ${opened.code} — ${opened.detail}`);
297
417
  const staging = await gateway.createSandbox({
@@ -342,7 +462,7 @@ var ModalTrainingProvider = class {
342
462
  submitJob = async (input) => {
343
463
  const existing = await this.deps.ledger.get(input.jobKey);
344
464
  if (existing?.sandboxId != null) return this.statusOf(existing);
345
- const gateway = this.requireGateway();
465
+ const gateway = this.requireGateway(input.entryId);
346
466
  const parsed = parseTrainerDockerfile(await this.deps.fetchText(trainerDockerfileUrl(input.trainer.repo, input.trainer.revision)), input.trainer.revision);
347
467
  if (parsed.kind === "refused") throw new Error(`trainer Dockerfile unusable on Modal: ${parsed.detail}`);
348
468
  const sandbox = await gateway.createSandbox({
@@ -368,12 +488,14 @@ var ModalTrainingProvider = class {
368
488
  },
369
489
  tags: {
370
490
  ...tagsFor(input.jobKey),
371
- role: "trainer"
491
+ role: "trainer",
492
+ entryId: input.entryId
372
493
  },
373
494
  name: `cst-${input.jobKey}`
374
495
  });
375
496
  const record = {
376
497
  jobKey: input.jobKey,
498
+ entryId: input.entryId,
377
499
  sandboxId: sandbox.sandboxId,
378
500
  gpuType: input.gpuType,
379
501
  region: input.region,
@@ -393,10 +515,10 @@ var ModalTrainingProvider = class {
393
515
  };
394
516
  getJob = async (input) => {
395
517
  const record = await this.deps.ledger.get(input.jobKey);
396
- if (record === null) return this.lost(input.jobKey);
518
+ if (record === null) return this.lost(input.jobKey, input.entryId);
397
519
  if (record.sandboxId === null || record.endedAt !== null) return this.statusOf(record);
398
- const sandbox = await this.requireGateway().sandboxFromId(record.sandboxId);
399
- if (sandbox === null) return this.lost(input.jobKey);
520
+ const sandbox = await this.requireGateway(record.entryId).sandboxFromId(record.sandboxId);
521
+ if (sandbox === null) return this.lost(input.jobKey, record.entryId);
400
522
  const code = await sandbox.poll();
401
523
  if (code === null) return this.statusOf(record);
402
524
  const ended = {
@@ -414,7 +536,7 @@ var ModalTrainingProvider = class {
414
536
  lines: [],
415
537
  cursor: `${String(cursor.ts)}:${String(cursor.seen)}`
416
538
  };
417
- const sandbox = await this.requireGateway().sandboxFromId(record.sandboxId);
539
+ const sandbox = await this.requireGateway(record.entryId).sandboxFromId(record.sandboxId);
418
540
  if (sandbox === null) return {
419
541
  lines: [],
420
542
  cursor: `${String(cursor.ts)}:${String(cursor.seen)}`
@@ -444,7 +566,7 @@ var ModalTrainingProvider = class {
444
566
  cancelJob = async (input) => {
445
567
  const record = await this.deps.ledger.get(input.jobKey);
446
568
  if (record?.sandboxId == null || record.endedAt !== null) return { ok: true };
447
- await (await this.requireGateway().sandboxFromId(record.sandboxId))?.terminate();
569
+ await (await this.requireGateway(record.entryId).sandboxFromId(record.sandboxId))?.terminate();
448
570
  await this.deps.ledger.put({
449
571
  ...record,
450
572
  endedAt: this.deps.now(),
@@ -453,7 +575,7 @@ var ModalTrainingProvider = class {
453
575
  return { ok: true };
454
576
  };
455
577
  readResult = async (input) => {
456
- const collector = await this.collector(input.jobKey);
578
+ const collector = await this.collector(input.entryId, input.jobKey);
457
579
  try {
458
580
  const { stdout } = await collector.execText([
459
581
  "sh",
@@ -473,7 +595,7 @@ var ModalTrainingProvider = class {
473
595
  const offered = await this.deps.peer.offer({
474
596
  label: "training-bundle",
475
597
  handler: async (_req, res) => {
476
- const collector = await this.collector(input.jobKey);
598
+ const collector = await this.collector(input.entryId, input.jobKey);
477
599
  try {
478
600
  const proc = await collector.execStream(["cat", `/data/out/${input.path}`]);
479
601
  res.writeHead(200, { "content-type": "application/x-tar" });
@@ -496,37 +618,66 @@ var ModalTrainingProvider = class {
496
618
  return { ticket: offered.ticket };
497
619
  };
498
620
  cleanupJob = async (input) => {
499
- const gateway = this.requireGateway();
621
+ const gateway = this.requireGateway(input.entryId);
500
622
  const deleted = [];
501
623
  for (const sb of await gateway.listSandboxes(tagsFor(input.jobKey))) await (await gateway.sandboxFromId(sb.sandboxId))?.terminate();
502
624
  if (await gateway.deleteVolume(jobVolumeName(input.jobKey))) deleted.push(jobVolumeName(input.jobKey));
503
625
  await this.deps.ledger.remove(input.jobKey);
504
626
  return { deleted };
505
627
  };
506
- listJobs = async () => {
507
- const records = await this.deps.ledger.list();
628
+ listJobs = async (input) => {
629
+ const records = (await this.deps.ledger.list()).filter((r) => input.entryId === void 0 || r.entryId === input.entryId);
508
630
  const known = new Set(records.map((r) => r.jobKey));
509
- const statuses = await Promise.all(records.map((r) => this.getJob({ jobKey: r.jobKey })));
510
- const gateway = this.deps.gateway();
631
+ const statuses = await Promise.all(records.map((r) => this.getJob({
632
+ entryId: r.entryId,
633
+ jobKey: r.jobKey
634
+ })));
635
+ const probeEntryId = input.entryId ?? this.deps.listEntries().find((e) => this.entryConfigured(e))?.id;
636
+ if (probeEntryId === void 0) return statuses;
637
+ const gateway = this.deps.gateway(probeEntryId);
511
638
  if (gateway === null) return statuses;
512
639
  const strays = (await gateway.listSandboxes({ camstack: "training" })).map((s) => s.tags["jobKey"]).filter((k) => k !== void 0 && !known.has(k));
513
640
  const unique = [...new Set(strays)];
514
641
  return [...statuses, ...unique.map((jobKey) => ({
515
- ...this.lost(jobKey),
642
+ ...this.lost(jobKey, probeEntryId),
516
643
  state: "running"
517
644
  }))];
518
645
  };
519
- configured() {
520
- const s = this.deps.settings();
521
- return s.tokenId.trim() !== "" && s.tokenSecret.trim() !== "";
646
+ entryConfigured(entry) {
647
+ return entry.tokenId.trim() !== "" && entry.tokenSecret.trim() !== "";
522
648
  }
523
- requireGateway() {
524
- const gateway = this.deps.gateway();
649
+ descriptorOf(entry) {
650
+ return {
651
+ addonId: this.deps.addonId,
652
+ entryId: entry.id,
653
+ typeId: MODAL_TYPE_ID,
654
+ name: entry.name,
655
+ iconAsset: MODAL_ICON_ASSET,
656
+ kind: MODAL_TYPE_ID,
657
+ label: "Modal",
658
+ gpuTypes: MODAL_GPUS,
659
+ regions: MODAL_REGIONS,
660
+ maxDatasetBytes: MODAL_MAX_DATASET_BYTES,
661
+ maxRuntimeSec: MODAL_MAX_RUNTIME_SEC,
662
+ freeTierNote: "The Starter plan includes $30 of credit every month; Modal bills per second.",
663
+ dataResidencyNote: "Modal keeps the frames on a Volume of your workspace for the length of the job; the training container sees them only while it runs.",
664
+ configured: this.entryConfigured(entry),
665
+ defaults: {
666
+ gpuType: entry.gpuType,
667
+ region: entry.region,
668
+ costCapUsd: entry.costCapUsd,
669
+ maxRuntimeSec: Math.round(entry.maxRuntimeHours * 3600),
670
+ failedDatasetGraceHours: entry.failedDatasetGraceHours
671
+ }
672
+ };
673
+ }
674
+ requireGateway(entryId) {
675
+ const gateway = this.deps.gateway(entryId);
525
676
  if (gateway === null) throw new Error("Modal is not configured: enter a token id and secret");
526
677
  return gateway;
527
678
  }
528
- collector(jobKey) {
529
- return this.requireGateway().createSandbox({
679
+ collector(entryId, jobKey) {
680
+ return this.requireGateway(entryId).createSandbox({
530
681
  image: {
531
682
  kind: "registry",
532
683
  tag: MODAL_TOOLS_IMAGE
@@ -538,7 +689,8 @@ var ModalTrainingProvider = class {
538
689
  volumes: { "/data": jobVolumeName(jobKey) },
539
690
  tags: {
540
691
  ...tagsFor(jobKey),
541
- role: "collector"
692
+ role: "collector",
693
+ entryId
542
694
  }
543
695
  });
544
696
  }
@@ -562,11 +714,11 @@ var ModalTrainingProvider = class {
562
714
  lastFetchedAt: now
563
715
  };
564
716
  }
565
- lost(jobKey) {
717
+ lost(jobKey, entryId) {
566
718
  return {
567
719
  jobKey,
568
720
  state: "lost",
569
- gpuType: this.deps.settings().gpuType,
721
+ gpuType: this.deps.entry(entryId)?.gpuType ?? "L4",
570
722
  billedSeconds: 0,
571
723
  costUsd: 0,
572
724
  costIsEstimate: true,
@@ -29750,7 +29902,7 @@ function rng() {
29750
29902
  }
29751
29903
  //#endregion
29752
29904
  //#region ../../node_modules/modal/node_modules/uuid/dist/esm/native.js
29753
- var native_default = { randomUUID };
29905
+ var native_default = { randomUUID: randomUUID$1 };
29754
29906
  //#endregion
29755
29907
  //#region ../../node_modules/modal/node_modules/uuid/dist/esm/v4.js
29756
29908
  function v4(options, buf, offset) {
@@ -88336,17 +88488,21 @@ var ModalSdkGateway = class {
88336
88488
  /**
88337
88489
  * `training-modal` — the Modal `training-provider` (D807).
88338
88490
  *
88339
- * Inert until the operator enters a Modal token (the `loki-logging`
88340
- * posture): the provider registers anyway so the Training page can list it as
88341
- * "not configured" and link here. Region, GPU, cost cap, maximum runtime and
88342
- * the failed-job grace period are THIS addon's settings (operator decision
88491
+ * Inert until the operator enters a Modal token on an **entry** (the
88492
+ * `loki-logging` posture): the provider registers anyway so the Training page
88493
+ * can list types and create entries. Region, GPU, cost cap, maximum runtime and
88494
+ * the failed-job grace period are per-entry settings (operator decision
88343
88495
  * 2026-10-08: they depend on the user's account). The token secret is a
88344
- * `password` field, so BaseAddon keeps it in the keychain (D787) — it is
88345
- * read from `this.config` and handed to the SDK client constructor only.
88496
+ * `password` field on each entry row, so BaseAddon keeps it in the keychain
88497
+ * (D787) — it is read from `this.config` and handed to the SDK client
88498
+ * constructor only.
88499
+ *
88500
+ * A pre-multi-entry flat config migrates to one entry named "Default" on boot.
88346
88501
  */
88347
88502
  var LEDGER_KEY = "modalJobs";
88348
88503
  var ModalJobRecordSchema = object({
88349
88504
  jobKey: string(),
88505
+ entryId: string().default(""),
88350
88506
  sandboxId: string().nullable(),
88351
88507
  gpuType: string(),
88352
88508
  region: string(),
@@ -88364,10 +88520,10 @@ async function fetchText(url) {
88364
88520
  }
88365
88521
  var TrainingModalAddon = class extends BaseAddon {
88366
88522
  ledgerState = null;
88367
- gateway = null;
88368
- gatewayKey = "";
88523
+ gateways = /* @__PURE__ */ new Map();
88369
88524
  constructor() {
88370
88525
  super({
88526
+ entries: [],
88371
88527
  tokenId: "",
88372
88528
  tokenSecret: "",
88373
88529
  environment: "",
@@ -88379,48 +88535,77 @@ var TrainingModalAddon = class extends BaseAddon {
88379
88535
  });
88380
88536
  }
88381
88537
  async onInitialize() {
88538
+ if (this.config.entries.length === 0) {
88539
+ const migrated = migrateLegacyModalConfig(this.config, () => randomUUID());
88540
+ if (migrated.entries.length > 0) await this.updateGlobalSettings(migrated);
88541
+ }
88382
88542
  this.ledgerState = this.state(LEDGER_KEY, LedgerSchema, {});
88383
88543
  const peer = this.ctx.peerBytes;
88384
88544
  if (peer === void 0) throw new Error("training-modal needs the peer-bytes facility");
88385
88545
  const provider = new ModalTrainingProvider({
88386
88546
  addonId: this.ctx.id,
88387
- settings: () => this.config,
88388
- gateway: () => this.currentGateway(),
88547
+ listEntries: () => effectiveModalEntries(this.config),
88548
+ entry: (entryId) => findModalEntry(this.config, entryId),
88549
+ gateway: (entryId) => this.gatewayFor(entryId),
88550
+ createEntry: async (input) => {
88551
+ const result = createModalEntry(this.config, input, () => randomUUID());
88552
+ if (!result.ok) throw new Error(`cannot create entry: ${result.reason}`);
88553
+ await this.updateGlobalSettings(result.config);
88554
+ return { entryId: result.entryId };
88555
+ },
88556
+ deleteEntry: async (entryId) => {
88557
+ const result = deleteModalEntry(this.config, entryId);
88558
+ if (!result.ok) throw new Error(`cannot delete entry: ${result.reason}`);
88559
+ await this.updateGlobalSettings(result.config);
88560
+ this.closeGateway(entryId);
88561
+ },
88389
88562
  ledger: this.ledger(),
88390
88563
  peer,
88391
88564
  fetchText,
88392
88565
  now: () => Date.now(),
88393
88566
  logger: this.ctx.logger
88394
88567
  });
88395
- this.ctx.logger.info("Modal training provider ready", { meta: { configured: this.config.tokenId.trim() !== "" && this.config.tokenSecret.trim() !== "" } });
88568
+ this.ctx.logger.info("Modal training provider ready", { meta: {
88569
+ entries: effectiveModalEntries(this.config).length,
88570
+ configured: effectiveModalEntries(this.config).some((e) => e.tokenId.trim() !== "" && e.tokenSecret.trim() !== "")
88571
+ } });
88396
88572
  return { providers: [{
88397
88573
  capability: trainingProviderCapability,
88398
88574
  provider
88399
88575
  }] };
88400
88576
  }
88401
88577
  async onShutdown() {
88402
- this.gateway?.close();
88403
- this.gateway = null;
88578
+ for (const { gateway } of this.gateways.values()) gateway.close();
88579
+ this.gateways.clear();
88404
88580
  }
88405
88581
  async onConfigChanged() {
88406
- this.gateway?.close();
88407
- this.gateway = null;
88408
- this.gatewayKey = "";
88409
- }
88410
- currentGateway() {
88411
- const { tokenId, tokenSecret, environment } = this.config;
88412
- if (tokenId.trim() === "" || tokenSecret.trim() === "") return null;
88413
- const key = `${tokenId}\u0000${environment}`;
88414
- if (this.gateway === null || this.gatewayKey !== key) {
88415
- this.gateway?.close();
88416
- this.gateway = new ModalSdkGateway({
88417
- tokenId: tokenId.trim(),
88418
- tokenSecret: tokenSecret.trim(),
88419
- environment: environment.trim()
88420
- });
88421
- this.gatewayKey = key;
88422
- }
88423
- return this.gateway;
88582
+ for (const { gateway } of this.gateways.values()) gateway.close();
88583
+ this.gateways.clear();
88584
+ }
88585
+ gatewayFor(entryId) {
88586
+ const entry = findModalEntry(this.config, entryId);
88587
+ if (entry === null) return null;
88588
+ if (entry.tokenId.trim() === "" || entry.tokenSecret.trim() === "") return null;
88589
+ const key = `${entry.tokenId}\u0000${entry.environment}`;
88590
+ const cached = this.gateways.get(entryId);
88591
+ if (cached !== void 0 && cached.key === key) return cached.gateway;
88592
+ cached?.gateway.close();
88593
+ const gateway = new ModalSdkGateway({
88594
+ tokenId: entry.tokenId.trim(),
88595
+ tokenSecret: entry.tokenSecret.trim(),
88596
+ environment: entry.environment.trim()
88597
+ });
88598
+ this.gateways.set(entryId, {
88599
+ key,
88600
+ gateway
88601
+ });
88602
+ return gateway;
88603
+ }
88604
+ closeGateway(entryId) {
88605
+ const cached = this.gateways.get(entryId);
88606
+ if (cached === void 0) return;
88607
+ cached.gateway.close();
88608
+ this.gateways.delete(entryId);
88424
88609
  }
88425
88610
  ledger() {
88426
88611
  const state = () => {
@@ -88446,80 +88631,101 @@ var TrainingModalAddon = class extends BaseAddon {
88446
88631
  }
88447
88632
  globalSettingsSchema() {
88448
88633
  return this.schema({ sections: [{
88449
- id: "account",
88450
- title: "Modal account",
88451
- description: "Create a token at modal.com → Settings → API tokens. The Starter plan is free and includes $30 of credit a month. The secret is kept in the CamStack keychain and only ever sent to Modal.",
88452
- fields: [
88453
- this.field({
88454
- type: "text",
88455
- key: "tokenId",
88456
- label: "Token id",
88457
- placeholder: "ak-…"
88458
- }),
88459
- this.field({
88460
- type: "password",
88461
- key: "tokenSecret",
88462
- label: "Token secret",
88463
- showToggle: true
88464
- }),
88465
- this.field({
88466
- type: "text",
88467
- key: "environment",
88468
- label: "Environment",
88469
- description: "Leave empty for the workspace default."
88470
- })
88471
- ]
88472
- }, {
88473
- id: "jobs",
88474
- title: "Jobs on this account",
88475
- description: "Defaults the Training page proposes for a job; each one can be changed before you confirm.",
88476
- fields: [
88477
- this.field({
88478
- type: "select",
88479
- key: "gpuType",
88480
- label: "GPU",
88481
- options: MODAL_GPUS.map((g) => ({
88482
- value: g.id,
88483
- label: `${g.label} — $${g.usdPerHour.toFixed(2)}/h`
88484
- }))
88485
- }),
88486
- this.field({
88487
- type: "select",
88488
- key: "region",
88489
- label: "Region",
88490
- description: "A pinned region costs more; the frames stay in your workspace either way.",
88491
- options: MODAL_REGIONS.map((r) => ({
88492
- value: r.id,
88493
- label: r.label
88494
- }))
88495
- }),
88496
- this.field({
88497
- type: "number",
88498
- key: "costCapUsd",
88499
- label: "Cost cap per job (USD)",
88500
- description: "A job is stopped when its estimated cost reaches this.",
88501
- min: .5,
88502
- step: .5
88503
- }),
88504
- this.field({
88505
- type: "number",
88506
- key: "maxRuntimeHours",
88507
- label: "Maximum runtime (hours)",
88508
- description: "Modal stops the container after this, whatever happens.",
88509
- min: .5,
88510
- max: 24,
88511
- step: .5
88512
- }),
88513
- this.field({
88514
- type: "number",
88515
- key: "failedDatasetGraceHours",
88516
- label: "Keep a failed job’s dataset (hours)",
88517
- description: "The frames are deleted from Modal as soon as a result is fetched. After a failure they are kept this long so the run can be inspected; 0 deletes them at once.",
88518
- min: 0,
88519
- max: 168,
88520
- step: 1
88521
- })
88522
- ]
88634
+ id: "entries",
88635
+ title: "Modal accounts",
88636
+ description: "Each entry is one Modal workspace token. Create entries from the Training → Providers tab; edit credentials here. Secrets stay in the CamStack keychain.",
88637
+ fields: [{
88638
+ type: "editable-array",
88639
+ key: "entries",
88640
+ label: "Entries",
88641
+ itemIdKey: "id",
88642
+ emptyMessage: "No Modal entries yet — add one from Training → Providers.",
88643
+ addLabel: "Add entry",
88644
+ rowTitleTemplate: "{name}",
88645
+ defaultItem: {
88646
+ id: "",
88647
+ name: "Default",
88648
+ tokenId: "",
88649
+ tokenSecret: "",
88650
+ environment: "",
88651
+ gpuType: "L4",
88652
+ region: "",
88653
+ costCapUsd: 5,
88654
+ maxRuntimeHours: 4,
88655
+ failedDatasetGraceHours: 24
88656
+ },
88657
+ itemFields: [
88658
+ {
88659
+ type: "text",
88660
+ key: "id",
88661
+ label: "Entry id"
88662
+ },
88663
+ {
88664
+ type: "text",
88665
+ key: "name",
88666
+ label: "Name"
88667
+ },
88668
+ {
88669
+ type: "text",
88670
+ key: "tokenId",
88671
+ label: "Token id",
88672
+ placeholder: "ak-…"
88673
+ },
88674
+ {
88675
+ type: "password",
88676
+ key: "tokenSecret",
88677
+ label: "Token secret",
88678
+ showToggle: true
88679
+ },
88680
+ {
88681
+ type: "text",
88682
+ key: "environment",
88683
+ label: "Environment",
88684
+ description: "Leave empty for the workspace default."
88685
+ },
88686
+ {
88687
+ type: "select",
88688
+ key: "gpuType",
88689
+ label: "GPU",
88690
+ options: MODAL_GPUS.map((g) => ({
88691
+ value: g.id,
88692
+ label: `${g.label} — $${g.usdPerHour.toFixed(2)}/h`
88693
+ }))
88694
+ },
88695
+ {
88696
+ type: "select",
88697
+ key: "region",
88698
+ label: "Region",
88699
+ options: MODAL_REGIONS.map((r) => ({
88700
+ value: r.id,
88701
+ label: r.label
88702
+ }))
88703
+ },
88704
+ {
88705
+ type: "number",
88706
+ key: "costCapUsd",
88707
+ label: "Cost cap per job (USD)",
88708
+ min: .5,
88709
+ step: .5
88710
+ },
88711
+ {
88712
+ type: "number",
88713
+ key: "maxRuntimeHours",
88714
+ label: "Maximum runtime (hours)",
88715
+ min: .5,
88716
+ max: 24,
88717
+ step: .5
88718
+ },
88719
+ {
88720
+ type: "number",
88721
+ key: "failedDatasetGraceHours",
88722
+ label: "Keep a failed job’s dataset (hours)",
88723
+ min: 0,
88724
+ max: 168,
88725
+ step: 1
88726
+ }
88727
+ ]
88728
+ }]
88523
88729
  }] });
88524
88730
  }
88525
88731
  };