@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,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("../../dist-BiSFqfpy.js");
2
+ const require_dist = require("../../dist-DVDBSrjw.js");
3
3
  let node_path = require("node:path");
4
4
  node_path = require_dist.__toESM(node_path, 1);
5
5
  let node_crypto = require("node:crypto");
@@ -11,6 +11,124 @@ let timers_promises = require("timers/promises");
11
11
  let node_console = require("node:console");
12
12
  let node_os = require("node:os");
13
13
  let node_stream = require("node:stream");
14
+ //#region src/providers/modal/modal-entries.ts
15
+ /**
16
+ * Modal provider entries — one credentials + defaults row per operator
17
+ * account. Pure helpers so the store is unit-tested without BaseAddon.
18
+ *
19
+ * A pre-multi-entry config (flat tokenId/tokenSecret on the addon) migrates
20
+ * to a single entry named {@link DEFAULT_MODAL_ENTRY_NAME}.
21
+ */
22
+ var MODAL_TYPE_ID = "modal";
23
+ var DEFAULT_MODAL_ENTRY_NAME = "Default";
24
+ var MODAL_ICON_ASSET = "assets/training-modal/icon.svg";
25
+ var DEFAULT_GPU = "L4";
26
+ var DEFAULT_COST = 5;
27
+ var DEFAULT_HOURS = 4;
28
+ var DEFAULT_GRACE = 24;
29
+ function legacyConfigured(config) {
30
+ return (config.tokenId ?? "").trim() !== "" || (config.tokenSecret ?? "").trim() !== "";
31
+ }
32
+ /** Entries the operator sees — synthesises Default from legacy flat fields. */
33
+ function effectiveModalEntries(config) {
34
+ if (config.entries.length > 0) return config.entries;
35
+ if (!legacyConfigured(config)) return [];
36
+ return [{
37
+ id: "legacy-default",
38
+ name: DEFAULT_MODAL_ENTRY_NAME,
39
+ tokenId: config.tokenId ?? "",
40
+ tokenSecret: config.tokenSecret ?? "",
41
+ environment: config.environment ?? "",
42
+ gpuType: config.gpuType ?? DEFAULT_GPU,
43
+ region: config.region ?? "",
44
+ costCapUsd: config.costCapUsd ?? DEFAULT_COST,
45
+ maxRuntimeHours: config.maxRuntimeHours ?? DEFAULT_HOURS,
46
+ failedDatasetGraceHours: config.failedDatasetGraceHours ?? DEFAULT_GRACE
47
+ }];
48
+ }
49
+ function findModalEntry(config, entryId) {
50
+ return effectiveModalEntries(config).find((e) => e.id === entryId) ?? null;
51
+ }
52
+ /** Persist a Default entry from legacy flat fields; clear the flat secrets. */
53
+ function migrateLegacyModalConfig(config, newId) {
54
+ if (config.entries.length > 0) return config;
55
+ if (!legacyConfigured(config)) return config;
56
+ const entry = {
57
+ id: newId(),
58
+ name: DEFAULT_MODAL_ENTRY_NAME,
59
+ tokenId: config.tokenId ?? "",
60
+ tokenSecret: config.tokenSecret ?? "",
61
+ environment: config.environment ?? "",
62
+ gpuType: config.gpuType ?? DEFAULT_GPU,
63
+ region: config.region ?? "",
64
+ costCapUsd: config.costCapUsd ?? DEFAULT_COST,
65
+ maxRuntimeHours: config.maxRuntimeHours ?? DEFAULT_HOURS,
66
+ failedDatasetGraceHours: config.failedDatasetGraceHours ?? DEFAULT_GRACE
67
+ };
68
+ return {
69
+ entries: [entry],
70
+ tokenId: "",
71
+ tokenSecret: "",
72
+ environment: "",
73
+ gpuType: entry.gpuType,
74
+ region: entry.region,
75
+ costCapUsd: entry.costCapUsd,
76
+ maxRuntimeHours: entry.maxRuntimeHours,
77
+ failedDatasetGraceHours: entry.failedDatasetGraceHours
78
+ };
79
+ }
80
+ function createModalEntry(config, input, newId) {
81
+ if (input.typeId !== "modal") return {
82
+ ok: false,
83
+ reason: "unknown-type"
84
+ };
85
+ const base = migrateLegacyModalConfig(config, newId);
86
+ const entry = {
87
+ id: newId(),
88
+ name: input.name.trim() === "" ? DEFAULT_MODAL_ENTRY_NAME : input.name.trim(),
89
+ tokenId: "",
90
+ tokenSecret: "",
91
+ environment: "",
92
+ gpuType: base.gpuType ?? DEFAULT_GPU,
93
+ region: base.region ?? "",
94
+ costCapUsd: base.costCapUsd ?? DEFAULT_COST,
95
+ maxRuntimeHours: base.maxRuntimeHours ?? DEFAULT_HOURS,
96
+ failedDatasetGraceHours: base.failedDatasetGraceHours ?? DEFAULT_GRACE
97
+ };
98
+ return {
99
+ ok: true,
100
+ entryId: entry.id,
101
+ config: {
102
+ ...base,
103
+ entries: [...base.entries, entry],
104
+ tokenId: "",
105
+ tokenSecret: ""
106
+ }
107
+ };
108
+ }
109
+ function deleteModalEntry(config, entryId) {
110
+ const current = effectiveModalEntries(config);
111
+ if (!current.some((e) => e.id === entryId)) return {
112
+ ok: false,
113
+ reason: "unknown-entry"
114
+ };
115
+ return {
116
+ ok: true,
117
+ entryId,
118
+ config: {
119
+ entries: current.filter((e) => e.id !== entryId),
120
+ tokenId: "",
121
+ tokenSecret: "",
122
+ environment: config.environment ?? "",
123
+ gpuType: config.gpuType ?? DEFAULT_GPU,
124
+ region: config.region ?? "",
125
+ costCapUsd: config.costCapUsd ?? DEFAULT_COST,
126
+ maxRuntimeHours: config.maxRuntimeHours ?? DEFAULT_HOURS,
127
+ failedDatasetGraceHours: config.failedDatasetGraceHours ?? DEFAULT_GRACE
128
+ }
129
+ };
130
+ }
131
+ //#endregion
14
132
  //#region src/providers/modal/dockerfile.ts
15
133
  var ALLOWED = new Set([
16
134
  "ARG",
@@ -206,30 +324,32 @@ var ModalTrainingProvider = class {
206
324
  constructor(deps) {
207
325
  this.deps = deps;
208
326
  }
209
- describe = async () => {
210
- const s = this.deps.settings();
211
- return [{
212
- addonId: this.deps.addonId,
213
- kind: "modal",
214
- label: "Modal",
215
- gpuTypes: MODAL_GPUS,
216
- regions: MODAL_REGIONS,
217
- maxDatasetBytes: MODAL_MAX_DATASET_BYTES,
218
- maxRuntimeSec: MODAL_MAX_RUNTIME_SEC,
219
- freeTierNote: "The Starter plan includes $30 of credit every month; Modal bills per second.",
220
- 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.",
221
- configured: this.configured(),
222
- defaults: {
223
- gpuType: s.gpuType,
224
- region: s.region,
225
- costCapUsd: s.costCapUsd,
226
- maxRuntimeSec: Math.round(s.maxRuntimeHours * 3600),
227
- failedDatasetGraceHours: s.failedDatasetGraceHours
228
- }
229
- }];
327
+ describe = async () => this.deps.listEntries().map((entry) => this.descriptorOf(entry));
328
+ describeTypes = async () => [{
329
+ typeId: MODAL_TYPE_ID,
330
+ label: "Modal",
331
+ iconAsset: MODAL_ICON_ASSET,
332
+ addonId: this.deps.addonId
333
+ }];
334
+ listEntries = async () => this.deps.listEntries().map((entry) => ({
335
+ entryId: entry.id,
336
+ typeId: MODAL_TYPE_ID,
337
+ name: entry.name,
338
+ addonId: this.deps.addonId,
339
+ configured: this.entryConfigured(entry)
340
+ }));
341
+ createEntry = async (input) => this.deps.createEntry(input);
342
+ deleteEntry = async (input) => {
343
+ await this.deps.deleteEntry(input.entryId);
344
+ return { ok: true };
230
345
  };
231
- validateCredentials = async () => {
232
- const gateway = this.deps.gateway();
346
+ validateCredentials = async (input) => {
347
+ if (this.deps.entry(input.entryId) === null) return {
348
+ ok: false,
349
+ reason: "missing",
350
+ detail: "unknown entry"
351
+ };
352
+ const gateway = this.deps.gateway(input.entryId);
233
353
  if (gateway === null) return {
234
354
  ok: false,
235
355
  reason: "missing",
@@ -263,7 +383,7 @@ var ModalTrainingProvider = class {
263
383
  };
264
384
  };
265
385
  uploadDataset = async (input) => {
266
- const gateway = this.requireGateway();
386
+ const gateway = this.requireGateway(input.entryId);
267
387
  const opened = await this.deps.peer.open(input.ticket);
268
388
  if (opened.kind === "refused") throw new Error(`dataset offer refused: ${opened.code} — ${opened.detail}`);
269
389
  const staging = await gateway.createSandbox({
@@ -314,7 +434,7 @@ var ModalTrainingProvider = class {
314
434
  submitJob = async (input) => {
315
435
  const existing = await this.deps.ledger.get(input.jobKey);
316
436
  if (existing?.sandboxId != null) return this.statusOf(existing);
317
- const gateway = this.requireGateway();
437
+ const gateway = this.requireGateway(input.entryId);
318
438
  const parsed = parseTrainerDockerfile(await this.deps.fetchText(trainerDockerfileUrl(input.trainer.repo, input.trainer.revision)), input.trainer.revision);
319
439
  if (parsed.kind === "refused") throw new Error(`trainer Dockerfile unusable on Modal: ${parsed.detail}`);
320
440
  const sandbox = await gateway.createSandbox({
@@ -340,12 +460,14 @@ var ModalTrainingProvider = class {
340
460
  },
341
461
  tags: {
342
462
  ...tagsFor(input.jobKey),
343
- role: "trainer"
463
+ role: "trainer",
464
+ entryId: input.entryId
344
465
  },
345
466
  name: `cst-${input.jobKey}`
346
467
  });
347
468
  const record = {
348
469
  jobKey: input.jobKey,
470
+ entryId: input.entryId,
349
471
  sandboxId: sandbox.sandboxId,
350
472
  gpuType: input.gpuType,
351
473
  region: input.region,
@@ -365,10 +487,10 @@ var ModalTrainingProvider = class {
365
487
  };
366
488
  getJob = async (input) => {
367
489
  const record = await this.deps.ledger.get(input.jobKey);
368
- if (record === null) return this.lost(input.jobKey);
490
+ if (record === null) return this.lost(input.jobKey, input.entryId);
369
491
  if (record.sandboxId === null || record.endedAt !== null) return this.statusOf(record);
370
- const sandbox = await this.requireGateway().sandboxFromId(record.sandboxId);
371
- if (sandbox === null) return this.lost(input.jobKey);
492
+ const sandbox = await this.requireGateway(record.entryId).sandboxFromId(record.sandboxId);
493
+ if (sandbox === null) return this.lost(input.jobKey, record.entryId);
372
494
  const code = await sandbox.poll();
373
495
  if (code === null) return this.statusOf(record);
374
496
  const ended = {
@@ -386,7 +508,7 @@ var ModalTrainingProvider = class {
386
508
  lines: [],
387
509
  cursor: `${String(cursor.ts)}:${String(cursor.seen)}`
388
510
  };
389
- const sandbox = await this.requireGateway().sandboxFromId(record.sandboxId);
511
+ const sandbox = await this.requireGateway(record.entryId).sandboxFromId(record.sandboxId);
390
512
  if (sandbox === null) return {
391
513
  lines: [],
392
514
  cursor: `${String(cursor.ts)}:${String(cursor.seen)}`
@@ -416,7 +538,7 @@ var ModalTrainingProvider = class {
416
538
  cancelJob = async (input) => {
417
539
  const record = await this.deps.ledger.get(input.jobKey);
418
540
  if (record?.sandboxId == null || record.endedAt !== null) return { ok: true };
419
- await (await this.requireGateway().sandboxFromId(record.sandboxId))?.terminate();
541
+ await (await this.requireGateway(record.entryId).sandboxFromId(record.sandboxId))?.terminate();
420
542
  await this.deps.ledger.put({
421
543
  ...record,
422
544
  endedAt: this.deps.now(),
@@ -425,7 +547,7 @@ var ModalTrainingProvider = class {
425
547
  return { ok: true };
426
548
  };
427
549
  readResult = async (input) => {
428
- const collector = await this.collector(input.jobKey);
550
+ const collector = await this.collector(input.entryId, input.jobKey);
429
551
  try {
430
552
  const { stdout } = await collector.execText([
431
553
  "sh",
@@ -445,7 +567,7 @@ var ModalTrainingProvider = class {
445
567
  const offered = await this.deps.peer.offer({
446
568
  label: "training-bundle",
447
569
  handler: async (_req, res) => {
448
- const collector = await this.collector(input.jobKey);
570
+ const collector = await this.collector(input.entryId, input.jobKey);
449
571
  try {
450
572
  const proc = await collector.execStream(["cat", `/data/out/${input.path}`]);
451
573
  res.writeHead(200, { "content-type": "application/x-tar" });
@@ -468,37 +590,66 @@ var ModalTrainingProvider = class {
468
590
  return { ticket: offered.ticket };
469
591
  };
470
592
  cleanupJob = async (input) => {
471
- const gateway = this.requireGateway();
593
+ const gateway = this.requireGateway(input.entryId);
472
594
  const deleted = [];
473
595
  for (const sb of await gateway.listSandboxes(tagsFor(input.jobKey))) await (await gateway.sandboxFromId(sb.sandboxId))?.terminate();
474
596
  if (await gateway.deleteVolume(jobVolumeName(input.jobKey))) deleted.push(jobVolumeName(input.jobKey));
475
597
  await this.deps.ledger.remove(input.jobKey);
476
598
  return { deleted };
477
599
  };
478
- listJobs = async () => {
479
- const records = await this.deps.ledger.list();
600
+ listJobs = async (input) => {
601
+ const records = (await this.deps.ledger.list()).filter((r) => input.entryId === void 0 || r.entryId === input.entryId);
480
602
  const known = new Set(records.map((r) => r.jobKey));
481
- const statuses = await Promise.all(records.map((r) => this.getJob({ jobKey: r.jobKey })));
482
- const gateway = this.deps.gateway();
603
+ const statuses = await Promise.all(records.map((r) => this.getJob({
604
+ entryId: r.entryId,
605
+ jobKey: r.jobKey
606
+ })));
607
+ const probeEntryId = input.entryId ?? this.deps.listEntries().find((e) => this.entryConfigured(e))?.id;
608
+ if (probeEntryId === void 0) return statuses;
609
+ const gateway = this.deps.gateway(probeEntryId);
483
610
  if (gateway === null) return statuses;
484
611
  const strays = (await gateway.listSandboxes({ camstack: "training" })).map((s) => s.tags["jobKey"]).filter((k) => k !== void 0 && !known.has(k));
485
612
  const unique = [...new Set(strays)];
486
613
  return [...statuses, ...unique.map((jobKey) => ({
487
- ...this.lost(jobKey),
614
+ ...this.lost(jobKey, probeEntryId),
488
615
  state: "running"
489
616
  }))];
490
617
  };
491
- configured() {
492
- const s = this.deps.settings();
493
- return s.tokenId.trim() !== "" && s.tokenSecret.trim() !== "";
618
+ entryConfigured(entry) {
619
+ return entry.tokenId.trim() !== "" && entry.tokenSecret.trim() !== "";
494
620
  }
495
- requireGateway() {
496
- const gateway = this.deps.gateway();
621
+ descriptorOf(entry) {
622
+ return {
623
+ addonId: this.deps.addonId,
624
+ entryId: entry.id,
625
+ typeId: MODAL_TYPE_ID,
626
+ name: entry.name,
627
+ iconAsset: MODAL_ICON_ASSET,
628
+ kind: MODAL_TYPE_ID,
629
+ label: "Modal",
630
+ gpuTypes: MODAL_GPUS,
631
+ regions: MODAL_REGIONS,
632
+ maxDatasetBytes: MODAL_MAX_DATASET_BYTES,
633
+ maxRuntimeSec: MODAL_MAX_RUNTIME_SEC,
634
+ freeTierNote: "The Starter plan includes $30 of credit every month; Modal bills per second.",
635
+ 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.",
636
+ configured: this.entryConfigured(entry),
637
+ defaults: {
638
+ gpuType: entry.gpuType,
639
+ region: entry.region,
640
+ costCapUsd: entry.costCapUsd,
641
+ maxRuntimeSec: Math.round(entry.maxRuntimeHours * 3600),
642
+ failedDatasetGraceHours: entry.failedDatasetGraceHours
643
+ }
644
+ };
645
+ }
646
+ requireGateway(entryId) {
647
+ const gateway = this.deps.gateway(entryId);
497
648
  if (gateway === null) throw new Error("Modal is not configured: enter a token id and secret");
498
649
  return gateway;
499
650
  }
500
- collector(jobKey) {
501
- return this.requireGateway().createSandbox({
651
+ collector(entryId, jobKey) {
652
+ return this.requireGateway(entryId).createSandbox({
502
653
  image: {
503
654
  kind: "registry",
504
655
  tag: MODAL_TOOLS_IMAGE
@@ -510,7 +661,8 @@ var ModalTrainingProvider = class {
510
661
  volumes: { "/data": jobVolumeName(jobKey) },
511
662
  tags: {
512
663
  ...tagsFor(jobKey),
513
- role: "collector"
664
+ role: "collector",
665
+ entryId
514
666
  }
515
667
  });
516
668
  }
@@ -534,11 +686,11 @@ var ModalTrainingProvider = class {
534
686
  lastFetchedAt: now
535
687
  };
536
688
  }
537
- lost(jobKey) {
689
+ lost(jobKey, entryId) {
538
690
  return {
539
691
  jobKey,
540
692
  state: "lost",
541
- gpuType: this.deps.settings().gpuType,
693
+ gpuType: this.deps.entry(entryId)?.gpuType ?? "L4",
542
694
  billedSeconds: 0,
543
695
  costUsd: 0,
544
696
  costIsEstimate: true,
@@ -88308,17 +88460,21 @@ var ModalSdkGateway = class {
88308
88460
  /**
88309
88461
  * `training-modal` — the Modal `training-provider` (D807).
88310
88462
  *
88311
- * Inert until the operator enters a Modal token (the `loki-logging`
88312
- * posture): the provider registers anyway so the Training page can list it as
88313
- * "not configured" and link here. Region, GPU, cost cap, maximum runtime and
88314
- * the failed-job grace period are THIS addon's settings (operator decision
88463
+ * Inert until the operator enters a Modal token on an **entry** (the
88464
+ * `loki-logging` posture): the provider registers anyway so the Training page
88465
+ * can list types and create entries. Region, GPU, cost cap, maximum runtime and
88466
+ * the failed-job grace period are per-entry settings (operator decision
88315
88467
  * 2026-10-08: they depend on the user's account). The token secret is a
88316
- * `password` field, so BaseAddon keeps it in the keychain (D787) — it is
88317
- * read from `this.config` and handed to the SDK client constructor only.
88468
+ * `password` field on each entry row, so BaseAddon keeps it in the keychain
88469
+ * (D787) — it is read from `this.config` and handed to the SDK client
88470
+ * constructor only.
88471
+ *
88472
+ * A pre-multi-entry flat config migrates to one entry named "Default" on boot.
88318
88473
  */
88319
88474
  var LEDGER_KEY = "modalJobs";
88320
88475
  var ModalJobRecordSchema = require_dist.object({
88321
88476
  jobKey: require_dist.string(),
88477
+ entryId: require_dist.string().default(""),
88322
88478
  sandboxId: require_dist.string().nullable(),
88323
88479
  gpuType: require_dist.string(),
88324
88480
  region: require_dist.string(),
@@ -88336,10 +88492,10 @@ async function fetchText(url) {
88336
88492
  }
88337
88493
  var TrainingModalAddon = class extends require_dist.BaseAddon {
88338
88494
  ledgerState = null;
88339
- gateway = null;
88340
- gatewayKey = "";
88495
+ gateways = /* @__PURE__ */ new Map();
88341
88496
  constructor() {
88342
88497
  super({
88498
+ entries: [],
88343
88499
  tokenId: "",
88344
88500
  tokenSecret: "",
88345
88501
  environment: "",
@@ -88351,48 +88507,77 @@ var TrainingModalAddon = class extends require_dist.BaseAddon {
88351
88507
  });
88352
88508
  }
88353
88509
  async onInitialize() {
88510
+ if (this.config.entries.length === 0) {
88511
+ const migrated = migrateLegacyModalConfig(this.config, () => (0, node_crypto.randomUUID)());
88512
+ if (migrated.entries.length > 0) await this.updateGlobalSettings(migrated);
88513
+ }
88354
88514
  this.ledgerState = this.state(LEDGER_KEY, LedgerSchema, {});
88355
88515
  const peer = this.ctx.peerBytes;
88356
88516
  if (peer === void 0) throw new Error("training-modal needs the peer-bytes facility");
88357
88517
  const provider = new ModalTrainingProvider({
88358
88518
  addonId: this.ctx.id,
88359
- settings: () => this.config,
88360
- gateway: () => this.currentGateway(),
88519
+ listEntries: () => effectiveModalEntries(this.config),
88520
+ entry: (entryId) => findModalEntry(this.config, entryId),
88521
+ gateway: (entryId) => this.gatewayFor(entryId),
88522
+ createEntry: async (input) => {
88523
+ const result = createModalEntry(this.config, input, () => (0, node_crypto.randomUUID)());
88524
+ if (!result.ok) throw new Error(`cannot create entry: ${result.reason}`);
88525
+ await this.updateGlobalSettings(result.config);
88526
+ return { entryId: result.entryId };
88527
+ },
88528
+ deleteEntry: async (entryId) => {
88529
+ const result = deleteModalEntry(this.config, entryId);
88530
+ if (!result.ok) throw new Error(`cannot delete entry: ${result.reason}`);
88531
+ await this.updateGlobalSettings(result.config);
88532
+ this.closeGateway(entryId);
88533
+ },
88361
88534
  ledger: this.ledger(),
88362
88535
  peer,
88363
88536
  fetchText,
88364
88537
  now: () => Date.now(),
88365
88538
  logger: this.ctx.logger
88366
88539
  });
88367
- this.ctx.logger.info("Modal training provider ready", { meta: { configured: this.config.tokenId.trim() !== "" && this.config.tokenSecret.trim() !== "" } });
88540
+ this.ctx.logger.info("Modal training provider ready", { meta: {
88541
+ entries: effectiveModalEntries(this.config).length,
88542
+ configured: effectiveModalEntries(this.config).some((e) => e.tokenId.trim() !== "" && e.tokenSecret.trim() !== "")
88543
+ } });
88368
88544
  return { providers: [{
88369
88545
  capability: require_dist.trainingProviderCapability,
88370
88546
  provider
88371
88547
  }] };
88372
88548
  }
88373
88549
  async onShutdown() {
88374
- this.gateway?.close();
88375
- this.gateway = null;
88550
+ for (const { gateway } of this.gateways.values()) gateway.close();
88551
+ this.gateways.clear();
88376
88552
  }
88377
88553
  async onConfigChanged() {
88378
- this.gateway?.close();
88379
- this.gateway = null;
88380
- this.gatewayKey = "";
88381
- }
88382
- currentGateway() {
88383
- const { tokenId, tokenSecret, environment } = this.config;
88384
- if (tokenId.trim() === "" || tokenSecret.trim() === "") return null;
88385
- const key = `${tokenId}\u0000${environment}`;
88386
- if (this.gateway === null || this.gatewayKey !== key) {
88387
- this.gateway?.close();
88388
- this.gateway = new ModalSdkGateway({
88389
- tokenId: tokenId.trim(),
88390
- tokenSecret: tokenSecret.trim(),
88391
- environment: environment.trim()
88392
- });
88393
- this.gatewayKey = key;
88394
- }
88395
- return this.gateway;
88554
+ for (const { gateway } of this.gateways.values()) gateway.close();
88555
+ this.gateways.clear();
88556
+ }
88557
+ gatewayFor(entryId) {
88558
+ const entry = findModalEntry(this.config, entryId);
88559
+ if (entry === null) return null;
88560
+ if (entry.tokenId.trim() === "" || entry.tokenSecret.trim() === "") return null;
88561
+ const key = `${entry.tokenId}\u0000${entry.environment}`;
88562
+ const cached = this.gateways.get(entryId);
88563
+ if (cached !== void 0 && cached.key === key) return cached.gateway;
88564
+ cached?.gateway.close();
88565
+ const gateway = new ModalSdkGateway({
88566
+ tokenId: entry.tokenId.trim(),
88567
+ tokenSecret: entry.tokenSecret.trim(),
88568
+ environment: entry.environment.trim()
88569
+ });
88570
+ this.gateways.set(entryId, {
88571
+ key,
88572
+ gateway
88573
+ });
88574
+ return gateway;
88575
+ }
88576
+ closeGateway(entryId) {
88577
+ const cached = this.gateways.get(entryId);
88578
+ if (cached === void 0) return;
88579
+ cached.gateway.close();
88580
+ this.gateways.delete(entryId);
88396
88581
  }
88397
88582
  ledger() {
88398
88583
  const state = () => {
@@ -88418,80 +88603,101 @@ var TrainingModalAddon = class extends require_dist.BaseAddon {
88418
88603
  }
88419
88604
  globalSettingsSchema() {
88420
88605
  return this.schema({ sections: [{
88421
- id: "account",
88422
- title: "Modal account",
88423
- 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.",
88424
- fields: [
88425
- this.field({
88426
- type: "text",
88427
- key: "tokenId",
88428
- label: "Token id",
88429
- placeholder: "ak-…"
88430
- }),
88431
- this.field({
88432
- type: "password",
88433
- key: "tokenSecret",
88434
- label: "Token secret",
88435
- showToggle: true
88436
- }),
88437
- this.field({
88438
- type: "text",
88439
- key: "environment",
88440
- label: "Environment",
88441
- description: "Leave empty for the workspace default."
88442
- })
88443
- ]
88444
- }, {
88445
- id: "jobs",
88446
- title: "Jobs on this account",
88447
- description: "Defaults the Training page proposes for a job; each one can be changed before you confirm.",
88448
- fields: [
88449
- this.field({
88450
- type: "select",
88451
- key: "gpuType",
88452
- label: "GPU",
88453
- options: MODAL_GPUS.map((g) => ({
88454
- value: g.id,
88455
- label: `${g.label} — $${g.usdPerHour.toFixed(2)}/h`
88456
- }))
88457
- }),
88458
- this.field({
88459
- type: "select",
88460
- key: "region",
88461
- label: "Region",
88462
- description: "A pinned region costs more; the frames stay in your workspace either way.",
88463
- options: MODAL_REGIONS.map((r) => ({
88464
- value: r.id,
88465
- label: r.label
88466
- }))
88467
- }),
88468
- this.field({
88469
- type: "number",
88470
- key: "costCapUsd",
88471
- label: "Cost cap per job (USD)",
88472
- description: "A job is stopped when its estimated cost reaches this.",
88473
- min: .5,
88474
- step: .5
88475
- }),
88476
- this.field({
88477
- type: "number",
88478
- key: "maxRuntimeHours",
88479
- label: "Maximum runtime (hours)",
88480
- description: "Modal stops the container after this, whatever happens.",
88481
- min: .5,
88482
- max: 24,
88483
- step: .5
88484
- }),
88485
- this.field({
88486
- type: "number",
88487
- key: "failedDatasetGraceHours",
88488
- label: "Keep a failed job’s dataset (hours)",
88489
- 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.",
88490
- min: 0,
88491
- max: 168,
88492
- step: 1
88493
- })
88494
- ]
88606
+ id: "entries",
88607
+ title: "Modal accounts",
88608
+ description: "Each entry is one Modal workspace token. Create entries from the Training → Providers tab; edit credentials here. Secrets stay in the CamStack keychain.",
88609
+ fields: [{
88610
+ type: "editable-array",
88611
+ key: "entries",
88612
+ label: "Entries",
88613
+ itemIdKey: "id",
88614
+ emptyMessage: "No Modal entries yet — add one from Training → Providers.",
88615
+ addLabel: "Add entry",
88616
+ rowTitleTemplate: "{name}",
88617
+ defaultItem: {
88618
+ id: "",
88619
+ name: "Default",
88620
+ tokenId: "",
88621
+ tokenSecret: "",
88622
+ environment: "",
88623
+ gpuType: "L4",
88624
+ region: "",
88625
+ costCapUsd: 5,
88626
+ maxRuntimeHours: 4,
88627
+ failedDatasetGraceHours: 24
88628
+ },
88629
+ itemFields: [
88630
+ {
88631
+ type: "text",
88632
+ key: "id",
88633
+ label: "Entry id"
88634
+ },
88635
+ {
88636
+ type: "text",
88637
+ key: "name",
88638
+ label: "Name"
88639
+ },
88640
+ {
88641
+ type: "text",
88642
+ key: "tokenId",
88643
+ label: "Token id",
88644
+ placeholder: "ak-…"
88645
+ },
88646
+ {
88647
+ type: "password",
88648
+ key: "tokenSecret",
88649
+ label: "Token secret",
88650
+ showToggle: true
88651
+ },
88652
+ {
88653
+ type: "text",
88654
+ key: "environment",
88655
+ label: "Environment",
88656
+ description: "Leave empty for the workspace default."
88657
+ },
88658
+ {
88659
+ type: "select",
88660
+ key: "gpuType",
88661
+ label: "GPU",
88662
+ options: MODAL_GPUS.map((g) => ({
88663
+ value: g.id,
88664
+ label: `${g.label} — $${g.usdPerHour.toFixed(2)}/h`
88665
+ }))
88666
+ },
88667
+ {
88668
+ type: "select",
88669
+ key: "region",
88670
+ label: "Region",
88671
+ options: MODAL_REGIONS.map((r) => ({
88672
+ value: r.id,
88673
+ label: r.label
88674
+ }))
88675
+ },
88676
+ {
88677
+ type: "number",
88678
+ key: "costCapUsd",
88679
+ label: "Cost cap per job (USD)",
88680
+ min: .5,
88681
+ step: .5
88682
+ },
88683
+ {
88684
+ type: "number",
88685
+ key: "maxRuntimeHours",
88686
+ label: "Maximum runtime (hours)",
88687
+ min: .5,
88688
+ max: 24,
88689
+ step: .5
88690
+ },
88691
+ {
88692
+ type: "number",
88693
+ key: "failedDatasetGraceHours",
88694
+ label: "Keep a failed job’s dataset (hours)",
88695
+ min: 0,
88696
+ max: 168,
88697
+ step: 1
88698
+ }
88699
+ ]
88700
+ }]
88495
88701
  }] });
88496
88702
  }
88497
88703
  };