From cc32f54cb99f5e49dd4154dcfb4d00bc976a4a99 Mon Sep 17 00:00:00 2001 From: hugo Date: Tue, 4 Aug 2026 14:44:03 +0200 Subject: [PATCH 1/5] feat: route to expose task options --- discojs/src/serialization/task.ts | 17 +++++++---------- server/src/routes/task_router.ts | 9 ++++++++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/discojs/src/serialization/task.ts b/discojs/src/serialization/task.ts index 86c5c6ba1..19d94ee09 100644 --- a/discojs/src/serialization/task.ts +++ b/discojs/src/serialization/task.ts @@ -25,17 +25,14 @@ export async function deserializeFromJSON( serialized: JSON, ): Promise> { return await z - .object({ - trainingInformation: z - .object({ - tokenizer: z - .string() - .transform((name) => Tokenizer.from_pretrained(name)) - .optional(), - }) - .passthrough(), + .looseObject({ + trainingInformation: z.looseObject({ + tokenizer: z + .string() + .transform((name) => Tokenizer.from_pretrained(name)) + .optional(), + }), }) - .passthrough() .pipe(Task.schema) .parseAsync(serialized); } diff --git a/server/src/routes/task_router.ts b/server/src/routes/task_router.ts index dea4b8c73..efa5d0515 100644 --- a/server/src/routes/task_router.ts +++ b/server/src/routes/task_router.ts @@ -3,7 +3,7 @@ import type { Request, Response } from "express"; import express from "express"; import { Set } from "immutable"; -import type { Task } from "@epfml/discojs"; +import { Task } from "@epfml/discojs"; import { serialization } from "@epfml/discojs"; import type { TaskSet } from "../task_set.js"; @@ -29,6 +29,13 @@ export class TaskRouter { ); }); + // Return the task schema to advertise available options + this.#expressRouter.get("/schema", (_, res) => { + res + .status(200) + .send(Task.schema.toJSONSchema({ unrepresentable: "any" })); + }); + this.#expressRouter.use(express.json()); // POST request to add a new task From 54bae020b3b254f9d0c5e7829b14396e991637eb Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 5 Aug 2026 13:05:57 +0200 Subject: [PATCH 2/5] refactor: model implementations to a subfolder, split models and tasks --- cli/src/args.ts | 1 + discojs/src/default_tasks/cifar10.ts | 4 +- discojs/src/default_tasks/lus_covid.ts | 2 + discojs/src/default_tasks/mnist.ts | 2 + discojs/src/default_tasks/simple_face.ts | 4 +- discojs/src/default_tasks/tinder_dog.ts | 2 + discojs/src/default_tasks/titanic.ts | 2 + discojs/src/default_tasks/wikitext.ts | 2 + discojs/src/index.ts | 2 + discojs/src/models/README.md | 15 +++++ discojs/src/models/cards/LUSClassifier.ts | 14 ++++ discojs/src/models/cards/dogClassifier.ts | 14 ++++ discojs/src/models/cards/faceClassifier.ts | 14 ++++ discojs/src/models/cards/index.ts | 7 ++ .../src/models/cards/mobileNetV1_cifar10.ts | 14 ++++ .../models/cards/pytorchMnistClassifier.ts | 14 ++++ discojs/src/models/cards/titanicClassifier.ts | 14 ++++ discojs/src/models/cards/wikitext.ts | 19 ++++++ .../implementations/LUSImageClassifier.ts | 67 +++++++++++++++++++ .../models/implementations/dog_classifier.ts | 57 ++++++++++++++++ .../{ => implementations}/gpt/LICENSE.md | 0 .../{ => implementations}/gpt/config.ts | 0 .../{ => implementations}/gpt/evaluate.ts | 0 .../{ => implementations}/gpt/gpt.spec.ts | 4 +- .../models/{ => implementations}/gpt/index.ts | 6 +- .../{ => implementations}/gpt/layers.spec.ts | 0 .../{ => implementations}/gpt/layers.ts | 0 .../models/{ => implementations}/gpt/model.ts | 0 .../{ => implementations}/gpt/optimizers.ts | 0 .../{ => implementations}/hellaswag.spec.ts | 4 +- .../models/{ => implementations}/hellaswag.ts | 6 +- .../implementations/mobileNetV1_cifar10.ts | 30 +++++++++ .../mobileNetV2_35_alpha_2_classes.ts | 0 .../mobileNetV2_face_classifier.ts | 18 +++++ .../mobileNet_v1_025_224.ts | 0 .../pytorch_mnist_classifier.ts | 36 ++++++++++ .../implementations/titanic_classifier.ts | 27 ++++++++ discojs/src/models/index.ts | 9 ++- discojs/src/models/model_card.ts | 20 ++++++ discojs/src/models/onnx.spec.ts | 2 +- discojs/src/models/onnx.ts | 4 +- discojs/src/task/task_provider.ts | 3 +- docs/examples/custom_task.ts | 36 +++++++++- server/tests/e2e/federated.spec.ts | 3 +- 44 files changed, 457 insertions(+), 21 deletions(-) create mode 100644 discojs/src/models/README.md create mode 100644 discojs/src/models/cards/LUSClassifier.ts create mode 100644 discojs/src/models/cards/dogClassifier.ts create mode 100644 discojs/src/models/cards/faceClassifier.ts create mode 100644 discojs/src/models/cards/index.ts create mode 100644 discojs/src/models/cards/mobileNetV1_cifar10.ts create mode 100644 discojs/src/models/cards/pytorchMnistClassifier.ts create mode 100644 discojs/src/models/cards/titanicClassifier.ts create mode 100644 discojs/src/models/cards/wikitext.ts create mode 100644 discojs/src/models/implementations/LUSImageClassifier.ts create mode 100644 discojs/src/models/implementations/dog_classifier.ts rename discojs/src/models/{ => implementations}/gpt/LICENSE.md (100%) rename discojs/src/models/{ => implementations}/gpt/config.ts (100%) rename discojs/src/models/{ => implementations}/gpt/evaluate.ts (100%) rename discojs/src/models/{ => implementations}/gpt/gpt.spec.ts (92%) rename discojs/src/models/{ => implementations}/gpt/index.ts (97%) rename discojs/src/models/{ => implementations}/gpt/layers.spec.ts (100%) rename discojs/src/models/{ => implementations}/gpt/layers.ts (100%) rename discojs/src/models/{ => implementations}/gpt/model.ts (100%) rename discojs/src/models/{ => implementations}/gpt/optimizers.ts (100%) rename discojs/src/models/{ => implementations}/hellaswag.spec.ts (96%) rename discojs/src/models/{ => implementations}/hellaswag.ts (98%) create mode 100644 discojs/src/models/implementations/mobileNetV1_cifar10.ts rename discojs/src/models/{ => implementations}/mobileNetV2_35_alpha_2_classes.ts (100%) create mode 100644 discojs/src/models/implementations/mobileNetV2_face_classifier.ts rename discojs/src/models/{ => implementations}/mobileNet_v1_025_224.ts (100%) create mode 100644 discojs/src/models/implementations/pytorch_mnist_classifier.ts create mode 100644 discojs/src/models/implementations/titanic_classifier.ts create mode 100644 discojs/src/models/model_card.ts diff --git a/cli/src/args.ts b/cli/src/args.ts index a40dcab9a..535cd34d7 100644 --- a/cli/src/args.ts +++ b/cli/src/args.ts @@ -280,5 +280,6 @@ export const args: BenchmarkArguments = { return task; }, getModel: () => provider.getModel(), + modelCard: provider.modelCard, }, }; diff --git a/discojs/src/default_tasks/cifar10.ts b/discojs/src/default_tasks/cifar10.ts index 79a9d7520..05f34d555 100644 --- a/discojs/src/default_tasks/cifar10.ts +++ b/discojs/src/default_tasks/cifar10.ts @@ -3,7 +3,7 @@ import * as tf from "@tensorflow/tfjs"; import type { Model, TaskProvider } from "../index.js"; import { models } from "../index.js"; -import baseModel from "../models/mobileNet_v1_025_224.js"; +import baseModel from "../models/implementations/mobileNet_v1_025_224.js"; export const cifar10: TaskProvider<"image", "decentralized"> = { getTask() { @@ -91,4 +91,6 @@ export const cifar10: TaskProvider<"image", "decentralized"> = { return new models.TFJS("image", model); }, + + modelCard: models.cards.CIFAR10Classifier, }; diff --git a/discojs/src/default_tasks/lus_covid.ts b/discojs/src/default_tasks/lus_covid.ts index eaee56885..5787317c0 100644 --- a/discojs/src/default_tasks/lus_covid.ts +++ b/discojs/src/default_tasks/lus_covid.ts @@ -107,4 +107,6 @@ export const lusCovid: TaskProvider<"image", "federated"> = { return Promise.resolve(new models.TFJS("image", model)); }, + + modelCard: models.cards.LUSClassifier, }; diff --git a/discojs/src/default_tasks/mnist.ts b/discojs/src/default_tasks/mnist.ts index 073359378..f15f73d1a 100644 --- a/discojs/src/default_tasks/mnist.ts +++ b/discojs/src/default_tasks/mnist.ts @@ -84,4 +84,6 @@ export const mnist: TaskProvider<"image", "decentralized"> = { return Promise.resolve(new models.TFJS("image", model)); }, + + modelCard: models.cards.PytorchMNISTClassifier, }; diff --git a/discojs/src/default_tasks/simple_face.ts b/discojs/src/default_tasks/simple_face.ts index ad45a8d8a..a1bc77564 100644 --- a/discojs/src/default_tasks/simple_face.ts +++ b/discojs/src/default_tasks/simple_face.ts @@ -2,7 +2,7 @@ import * as tf from "@tensorflow/tfjs"; import type { Model, TaskProvider } from "../index.js"; import { models } from "../index.js"; -import baseModel from "../models/mobileNetV2_35_alpha_2_classes.js"; +import baseModel from "../models/implementations/mobileNetV2_35_alpha_2_classes.js"; export const simpleFace: TaskProvider<"image", "federated"> = { getTask() { @@ -55,4 +55,6 @@ export const simpleFace: TaskProvider<"image", "federated"> = { return new models.TFJS("image", model); }, + + modelCard: models.cards.FaceClassifier, }; diff --git a/discojs/src/default_tasks/tinder_dog.ts b/discojs/src/default_tasks/tinder_dog.ts index e59c347fe..b433a0857 100644 --- a/discojs/src/default_tasks/tinder_dog.ts +++ b/discojs/src/default_tasks/tinder_dog.ts @@ -97,4 +97,6 @@ export const tinderDog: TaskProvider<"image", "federated"> = { return Promise.resolve(new models.TFJS("image", model)); }, + + modelCard: models.cards.DogClassifier, }; diff --git a/discojs/src/default_tasks/titanic.ts b/discojs/src/default_tasks/titanic.ts index 4908e21e1..ece837957 100644 --- a/discojs/src/default_tasks/titanic.ts +++ b/discojs/src/default_tasks/titanic.ts @@ -78,4 +78,6 @@ export const titanic: TaskProvider<"tabular", "federated"> = { return Promise.resolve(new models.TFJS("tabular", model)); }, + + modelCard: models.cards.TitanicClassifier, }; diff --git a/discojs/src/default_tasks/wikitext.ts b/discojs/src/default_tasks/wikitext.ts index 354cb24d9..c87e6b9fa 100644 --- a/discojs/src/default_tasks/wikitext.ts +++ b/discojs/src/default_tasks/wikitext.ts @@ -55,4 +55,6 @@ export const wikitext: TaskProvider<"text", "federated"> = { contextLength: task.trainingInformation.contextLength, }); }, + + modelCard: models.cards.Wikitext, }; diff --git a/discojs/src/index.ts b/discojs/src/index.ts index 1930aa14c..77baf09a8 100644 --- a/discojs/src/index.ts +++ b/discojs/src/index.ts @@ -23,10 +23,12 @@ export { Tokenizer, ValidationMetrics, } from "./models/index.js"; +export { ModelCard } from "./models/index.js"; export * as models from "./models/index.js"; export * from "./task/index.js"; export * as defaultTasks from "./default_tasks/index.js"; +export * as defaultModels from "./models/cards/index.js"; export * as async_iterator from "./utils/async_iterator.js"; export { EventEmitter } from "./utils/event_emitter.js"; diff --git a/discojs/src/models/README.md b/discojs/src/models/README.md new file mode 100644 index 000000000..244254ff0 --- /dev/null +++ b/discojs/src/models/README.md @@ -0,0 +1,15 @@ +# Models + +This folder contains everything related to models. + +## Abstract model classes + +Defined in the root. `Model` is the most basic class, which then gets extended by `TFJS`, `ONNXModel`, `GPT` and others in the future. + +## Model cards + +This is where models are wrapped so they can be exposed as available, and linked to tasks. + +## Model implementations + +Concrete model implementations (e.g. Keras code converted model JSON definition) go here diff --git a/discojs/src/models/cards/LUSClassifier.ts b/discojs/src/models/cards/LUSClassifier.ts new file mode 100644 index 000000000..cd24caef1 --- /dev/null +++ b/discojs/src/models/cards/LUSClassifier.ts @@ -0,0 +1,14 @@ +import { Model, ModelCard } from "../index.js"; +import { model } from "../implementations/LUSImageClassifier.js"; + +export const LUSClassifier: ModelCard<"image"> = { + card: { + id: "lus_classifier", + name: "LUS Classifier", + preTrained: false, + }, + + async getModel(): Promise> { + return Promise.resolve(model()); + }, +}; diff --git a/discojs/src/models/cards/dogClassifier.ts b/discojs/src/models/cards/dogClassifier.ts new file mode 100644 index 000000000..9cbf17bda --- /dev/null +++ b/discojs/src/models/cards/dogClassifier.ts @@ -0,0 +1,14 @@ +import { Model, ModelCard } from "../index.js"; +import { model } from "../implementations/dog_classifier.js"; + +export const DogClassifier: ModelCard<"image"> = { + card: { + id: "dog_classifier", + name: "Dog Classifier", + preTrained: false, + }, + + async getModel(): Promise> { + return Promise.resolve(model()); + }, +}; diff --git a/discojs/src/models/cards/faceClassifier.ts b/discojs/src/models/cards/faceClassifier.ts new file mode 100644 index 000000000..b8655d8cf --- /dev/null +++ b/discojs/src/models/cards/faceClassifier.ts @@ -0,0 +1,14 @@ +import { Model, ModelCard } from "../index.js"; +import { getModel } from "../implementations/mobileNetV2_face_classifier.js"; + +export const FaceClassifier: ModelCard<"image"> = { + card: { + id: "face_classifier", + name: "Face Classifier", + preTrained: false, + }, + + async getModel(): Promise> { + return await getModel(); + }, +}; diff --git a/discojs/src/models/cards/index.ts b/discojs/src/models/cards/index.ts new file mode 100644 index 000000000..ae9957342 --- /dev/null +++ b/discojs/src/models/cards/index.ts @@ -0,0 +1,7 @@ +export { LUSClassifier } from "./LUSClassifier.js"; +export { CIFAR10Classifier } from "./mobileNetV1_cifar10.js"; +export { PytorchMNISTClassifier } from "./pytorchMnistClassifier.js"; +export { FaceClassifier } from "./faceClassifier.js"; +export { DogClassifier } from "./dogClassifier.js"; +export { TitanicClassifier } from "./titanicClassifier.js"; +export { Wikitext } from "./wikitext.js"; diff --git a/discojs/src/models/cards/mobileNetV1_cifar10.ts b/discojs/src/models/cards/mobileNetV1_cifar10.ts new file mode 100644 index 000000000..56458c1e8 --- /dev/null +++ b/discojs/src/models/cards/mobileNetV1_cifar10.ts @@ -0,0 +1,14 @@ +import { Model, ModelCard } from "../index.js"; +import { getModel } from "../implementations/mobileNetV1_cifar10.js"; + +export const CIFAR10Classifier: ModelCard<"image"> = { + card: { + id: "mobile_net_v1_cifar10", + name: "Pre-trained CIFAR-10 Classifier", + preTrained: true, + }, + + async getModel(): Promise> { + return await getModel(); + }, +}; diff --git a/discojs/src/models/cards/pytorchMnistClassifier.ts b/discojs/src/models/cards/pytorchMnistClassifier.ts new file mode 100644 index 000000000..dfd10a6f0 --- /dev/null +++ b/discojs/src/models/cards/pytorchMnistClassifier.ts @@ -0,0 +1,14 @@ +import { Model, ModelCard } from "../index.js"; +import { model } from "../implementations/pytorch_mnist_classifier.js"; + +export const PytorchMNISTClassifier: ModelCard<"image"> = { + card: { + id: "mnist_classifier", + name: "MNIST Classifier", + preTrained: false, + }, + + async getModel(): Promise> { + return Promise.resolve(model()); + }, +}; diff --git a/discojs/src/models/cards/titanicClassifier.ts b/discojs/src/models/cards/titanicClassifier.ts new file mode 100644 index 000000000..053d8f8bb --- /dev/null +++ b/discojs/src/models/cards/titanicClassifier.ts @@ -0,0 +1,14 @@ +import { Model, ModelCard } from "../index.js"; +import { model } from "../implementations/titanic_classifier.js"; + +export const TitanicClassifier: ModelCard<"tabular"> = { + card: { + id: "titanic_classifier", + name: "Titanic classifier", + preTrained: false, + }, + + async getModel(): Promise> { + return Promise.resolve(model()); + }, +}; diff --git a/discojs/src/models/cards/wikitext.ts b/discojs/src/models/cards/wikitext.ts new file mode 100644 index 000000000..b617507f3 --- /dev/null +++ b/discojs/src/models/cards/wikitext.ts @@ -0,0 +1,19 @@ +import { Model, ModelCard } from "../index.js"; +import { GPT } from "../index.js"; + +export const Wikitext: ModelCard<"text"> = { + card: { + id: "wikitext", + name: "Wikitext GPT-2", + preTrained: false, + contextLength: 64, + }, + + async getModel(): Promise> { + const model = new GPT({ + contextLength: this.card.contextLength, + }); + + return Promise.resolve(model); + }, +}; diff --git a/discojs/src/models/implementations/LUSImageClassifier.ts b/discojs/src/models/implementations/LUSImageClassifier.ts new file mode 100644 index 000000000..f2e480697 --- /dev/null +++ b/discojs/src/models/implementations/LUSImageClassifier.ts @@ -0,0 +1,67 @@ +import * as tf from "@tensorflow/tfjs"; + +import { Model, TFJS } from "../index.js"; + +// Model architecture from tensorflow.js docs: +// https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/index.html#4 +export function model(): Model<"image"> { + const imageHeight = 100; + const imageWidth = 100; + const imageChannels = 3; + const numOutputClasses = 2; + const model = tf.sequential(); + + // In the first layer of our convolutional neural network we have + // to specify the input shape. Then we specify some parameters for + // the convolution operation that takes place in this layer. + model.add( + tf.layers.conv2d({ + inputShape: [imageHeight, imageWidth, imageChannels], + kernelSize: 5, + filters: 8, + strides: 1, + activation: "relu", + kernelInitializer: "varianceScaling", + }), + ); + + // The MaxPooling layer acts as a sort of downsampling using max values + // in a region instead of averaging. + model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] })); + + // Repeat the conv2d + maxPooling block. + // Note that we have more filters in the convolution. + model.add( + tf.layers.conv2d({ + kernelSize: 5, + filters: 16, + strides: 1, + activation: "relu", + kernelInitializer: "varianceScaling", + }), + ); + model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] })); + + // Now we flatten the output from the 2D filters into a 1D vector to prepare + // it for input into our last layer. This is common practice when feeding + // higher dimensional data to a final classification output layer. + model.add(tf.layers.flatten()); + + // Our last layer is a dense layer which has 2 output units, one for each + // output class. + model.add( + tf.layers.dense({ + units: numOutputClasses, + kernelInitializer: "varianceScaling", + activation: "softmax", + }), + ); + + model.compile({ + optimizer: "sgd", + loss: "binaryCrossentropy", + metrics: ["accuracy"], + }); + + return new TFJS("image", model); +} diff --git a/discojs/src/models/implementations/dog_classifier.ts b/discojs/src/models/implementations/dog_classifier.ts new file mode 100644 index 000000000..ff66091d9 --- /dev/null +++ b/discojs/src/models/implementations/dog_classifier.ts @@ -0,0 +1,57 @@ +import * as tf from "@tensorflow/tfjs"; + +import { TFJS } from "../index.js"; + +export function model() { + const seed = 42; // set a seed to ensure reproducibility during GDHF demo + const imageHeight = 64; + const imageWidth = 64; + const imageChannels = 3; + + const model = tf.sequential(); + + model.add( + tf.layers.conv2d({ + inputShape: [imageHeight, imageWidth, imageChannels], + kernelSize: 5, + filters: 8, + activation: "relu", + kernelInitializer: tf.initializers.heNormal({ seed }), + }), + ); + model.add( + tf.layers.conv2d({ + kernelSize: 5, + filters: 16, + activation: "relu", + kernelInitializer: tf.initializers.heNormal({ seed }), + }), + ); + model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 })); + model.add(tf.layers.dropout({ rate: 0.25, seed })); + + model.add(tf.layers.flatten()); + model.add( + tf.layers.dense({ + units: 32, + activation: "relu", + kernelInitializer: tf.initializers.heNormal({ seed }), + }), + ); + model.add(tf.layers.dropout({ rate: 0.25, seed })); + model.add( + tf.layers.dense({ + units: 2, + activation: "softmax", + kernelInitializer: tf.initializers.heNormal({ seed }), + }), + ); + + model.compile({ + optimizer: tf.train.adam(0.0005), + loss: "categoricalCrossentropy", + metrics: ["accuracy"], + }); + + return new TFJS("image", model); +} diff --git a/discojs/src/models/gpt/LICENSE.md b/discojs/src/models/implementations/gpt/LICENSE.md similarity index 100% rename from discojs/src/models/gpt/LICENSE.md rename to discojs/src/models/implementations/gpt/LICENSE.md diff --git a/discojs/src/models/gpt/config.ts b/discojs/src/models/implementations/gpt/config.ts similarity index 100% rename from discojs/src/models/gpt/config.ts rename to discojs/src/models/implementations/gpt/config.ts diff --git a/discojs/src/models/gpt/evaluate.ts b/discojs/src/models/implementations/gpt/evaluate.ts similarity index 100% rename from discojs/src/models/gpt/evaluate.ts rename to discojs/src/models/implementations/gpt/evaluate.ts diff --git a/discojs/src/models/gpt/gpt.spec.ts b/discojs/src/models/implementations/gpt/gpt.spec.ts similarity index 92% rename from discojs/src/models/gpt/gpt.spec.ts rename to discojs/src/models/implementations/gpt/gpt.spec.ts index 83a36e90d..c7e775545 100644 --- a/discojs/src/models/gpt/gpt.spec.ts +++ b/discojs/src/models/implementations/gpt/gpt.spec.ts @@ -1,8 +1,8 @@ import { List } from "immutable"; import { describe, expect, it } from "vitest"; -import type { DataFormat } from "../../index.js"; -import { Dataset, Tokenizer } from "../../index.js"; +import type { DataFormat } from "../../../index.js"; +import { Dataset, Tokenizer } from "../../../index.js"; import { GPT } from "./index.js"; diff --git a/discojs/src/models/gpt/index.ts b/discojs/src/models/implementations/gpt/index.ts similarity index 97% rename from discojs/src/models/gpt/index.ts rename to discojs/src/models/implementations/gpt/index.ts index 7c6d07911..ad13e4b73 100644 --- a/discojs/src/models/gpt/index.ts +++ b/discojs/src/models/implementations/gpt/index.ts @@ -7,10 +7,10 @@ import createDebug from "debug"; import { List, Range } from "immutable"; import * as tf from "@tensorflow/tfjs"; -import type { Batched, Dataset, DataFormat } from "../../index.js"; -import { WeightsContainer } from "../../index.js"; +import type { Batched, Dataset, DataFormat } from "../../../index.js"; +import { WeightsContainer } from "../../../index.js"; -import { BatchLogs, Model, EpochLogs } from "../index.js"; +import { BatchLogs, Model, EpochLogs } from "../../index.js"; import { GPTModel } from "./model.js"; import evaluate from "./evaluate.js"; diff --git a/discojs/src/models/gpt/layers.spec.ts b/discojs/src/models/implementations/gpt/layers.spec.ts similarity index 100% rename from discojs/src/models/gpt/layers.spec.ts rename to discojs/src/models/implementations/gpt/layers.spec.ts diff --git a/discojs/src/models/gpt/layers.ts b/discojs/src/models/implementations/gpt/layers.ts similarity index 100% rename from discojs/src/models/gpt/layers.ts rename to discojs/src/models/implementations/gpt/layers.ts diff --git a/discojs/src/models/gpt/model.ts b/discojs/src/models/implementations/gpt/model.ts similarity index 100% rename from discojs/src/models/gpt/model.ts rename to discojs/src/models/implementations/gpt/model.ts diff --git a/discojs/src/models/gpt/optimizers.ts b/discojs/src/models/implementations/gpt/optimizers.ts similarity index 100% rename from discojs/src/models/gpt/optimizers.ts rename to discojs/src/models/implementations/gpt/optimizers.ts diff --git a/discojs/src/models/hellaswag.spec.ts b/discojs/src/models/implementations/hellaswag.spec.ts similarity index 96% rename from discojs/src/models/hellaswag.spec.ts rename to discojs/src/models/implementations/hellaswag.spec.ts index fc0b92f5d..b5daab828 100644 --- a/discojs/src/models/hellaswag.spec.ts +++ b/discojs/src/models/implementations/hellaswag.spec.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import { evaluate } from "./hellaswag.js"; -import { GPT, Tokenizer } from "./index.js"; -import { ONNXModel } from "./onnx.js"; +import { GPT, Tokenizer } from "../index.js"; +import { ONNXModel } from "../onnx.js"; import type { HellaSwagExample } from "./hellaswag.js"; const exampleDataset: HellaSwagExample[] = [ diff --git a/discojs/src/models/hellaswag.ts b/discojs/src/models/implementations/hellaswag.ts similarity index 98% rename from discojs/src/models/hellaswag.ts rename to discojs/src/models/implementations/hellaswag.ts index 0c9032eeb..b1447fe3a 100644 --- a/discojs/src/models/hellaswag.ts +++ b/discojs/src/models/implementations/hellaswag.ts @@ -1,8 +1,8 @@ import * as tf from "@tensorflow/tfjs"; import { List } from "immutable"; -import type { Tokenizer } from "./index.js"; -import { GPT } from "./index.js"; -import type { ONNXModel } from "./onnx.js"; +import type { Tokenizer } from "../index.js"; +import { GPT } from "../index.js"; +import type { ONNXModel } from "../onnx.js"; export const HELLASWAG_URL = "https://raw.githubusercontent.com/rowanz/hellaswag/master/data/hellaswag_val.jsonl"; diff --git a/discojs/src/models/implementations/mobileNetV1_cifar10.ts b/discojs/src/models/implementations/mobileNetV1_cifar10.ts new file mode 100644 index 000000000..dfb5dd1a8 --- /dev/null +++ b/discojs/src/models/implementations/mobileNetV1_cifar10.ts @@ -0,0 +1,30 @@ +import * as tf from "@tensorflow/tfjs"; + +import { TFJS } from "../index.js"; + +import baseModel from "./mobileNet_v1_025_224.js"; + +export async function getModel() { + const mobilenet = await tf.loadLayersModel({ + load: async () => Promise.resolve(baseModel), + }); + + const x = mobilenet.getLayer("global_average_pooling2d_1"); + const predictions = tf.layers + .dense({ units: 10, activation: "softmax", name: "denseModified" }) + .apply(x.output) as tf.SymbolicTensor; + + const model = tf.model({ + inputs: mobilenet.input, + outputs: predictions, + name: "modelModified", + }); + + model.compile({ + optimizer: "sgd", + loss: "categoricalCrossentropy", + metrics: ["accuracy"], + }); + + return new TFJS("image", model); +} diff --git a/discojs/src/models/mobileNetV2_35_alpha_2_classes.ts b/discojs/src/models/implementations/mobileNetV2_35_alpha_2_classes.ts similarity index 100% rename from discojs/src/models/mobileNetV2_35_alpha_2_classes.ts rename to discojs/src/models/implementations/mobileNetV2_35_alpha_2_classes.ts diff --git a/discojs/src/models/implementations/mobileNetV2_face_classifier.ts b/discojs/src/models/implementations/mobileNetV2_face_classifier.ts new file mode 100644 index 000000000..8894e5d54 --- /dev/null +++ b/discojs/src/models/implementations/mobileNetV2_face_classifier.ts @@ -0,0 +1,18 @@ +import * as tf from "@tensorflow/tfjs"; + +import { TFJS } from "../index.js"; +import baseModel from "./mobileNetV2_35_alpha_2_classes.js"; + +export async function getModel() { + const model = await tf.loadLayersModel({ + load: async () => Promise.resolve(baseModel), + }); + + model.compile({ + optimizer: tf.train.sgd(0.001), + loss: "categoricalCrossentropy", + metrics: ["accuracy"], + }); + + return new TFJS("image", model); +} diff --git a/discojs/src/models/mobileNet_v1_025_224.ts b/discojs/src/models/implementations/mobileNet_v1_025_224.ts similarity index 100% rename from discojs/src/models/mobileNet_v1_025_224.ts rename to discojs/src/models/implementations/mobileNet_v1_025_224.ts diff --git a/discojs/src/models/implementations/pytorch_mnist_classifier.ts b/discojs/src/models/implementations/pytorch_mnist_classifier.ts new file mode 100644 index 000000000..582874f8f --- /dev/null +++ b/discojs/src/models/implementations/pytorch_mnist_classifier.ts @@ -0,0 +1,36 @@ +import * as tf from "@tensorflow/tfjs"; + +import { TFJS } from "../index.js"; + +export function model() { + // Architecture from the PyTorch MNIST example (I made it slightly smaller, 650kB instead of 5MB) + // https://github.com/pytorch/examples/blob/main/mnist/main.py + const model = tf.sequential(); + + model.add( + tf.layers.conv2d({ + inputShape: [28, 28, 3], + kernelSize: 5, + filters: 8, + activation: "relu", + }), + ); + model.add( + tf.layers.conv2d({ kernelSize: 5, filters: 16, activation: "relu" }), + ); + model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 })); + model.add(tf.layers.dropout({ rate: 0.25 })); + + model.add(tf.layers.flatten()); + model.add(tf.layers.dense({ units: 32, activation: "relu" })); + model.add(tf.layers.dropout({ rate: 0.25 })); + model.add(tf.layers.dense({ units: 10, activation: "softmax" })); + + model.compile({ + optimizer: "adam", + loss: "categoricalCrossentropy", + metrics: ["accuracy"], + }); + + return new TFJS("image", model); +} diff --git a/discojs/src/models/implementations/titanic_classifier.ts b/discojs/src/models/implementations/titanic_classifier.ts new file mode 100644 index 000000000..9013f1565 --- /dev/null +++ b/discojs/src/models/implementations/titanic_classifier.ts @@ -0,0 +1,27 @@ +import * as tf from "@tensorflow/tfjs"; + +import { TFJS } from "../index.js"; + +export function model() { + const model = tf.sequential(); + + model.add( + tf.layers.dense({ + inputShape: [5], + units: 124, + activation: "relu", + kernelInitializer: "leCunNormal", + }), + ); + model.add(tf.layers.dense({ units: 64, activation: "relu" })); + model.add(tf.layers.dense({ units: 32, activation: "relu" })); + model.add(tf.layers.dense({ units: 1, activation: "sigmoid" })); + + model.compile({ + optimizer: "adam", + loss: "binaryCrossentropy", + metrics: ["accuracy"], + }); + + return new TFJS("tabular", model); +} diff --git a/discojs/src/models/index.ts b/discojs/src/models/index.ts index 9f53bf6c3..40441c4ca 100644 --- a/discojs/src/models/index.ts +++ b/discojs/src/models/index.ts @@ -2,13 +2,16 @@ export { Model } from "./model.js"; export { BatchLogs, EpochLogs, ValidationMetrics } from "./logs.js"; export { Tokenizer } from "./tokenizer.js"; -export { GPT } from "./gpt/index.js"; +export { GPT } from "./implementations/gpt/index.js"; export { ONNXModel } from "./onnx.js"; -export { GPTConfig } from "./gpt/config.js"; +export { GPTConfig } from "./implementations/gpt/config.js"; export { evaluate as evaluate_hellaswag, HellaSwagDataset, HellaSwagExample, HELLASWAG_URL, -} from "./hellaswag.js"; +} from "./implementations/hellaswag.js"; export { TFJS } from "./tfjs.js"; +export { ModelCard } from "./model_card.js"; + +export * as cards from "./cards/index.js"; diff --git a/discojs/src/models/model_card.ts b/discojs/src/models/model_card.ts new file mode 100644 index 000000000..2996e99f2 --- /dev/null +++ b/discojs/src/models/model_card.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import type { DataType, Model } from "../index.js"; + +namespace ModelCardInfo { + export type ID = string; + + export const schema = z.object({ + id: z.string(), + name: z.string(), + preTrained: z.boolean(), + contextLength: z.number().optional(), + }); +} + +type ModelCardInfo = z.infer; + +export interface ModelCard { + card: ModelCardInfo; + getModel(): Promise>; +} diff --git a/discojs/src/models/onnx.spec.ts b/discojs/src/models/onnx.spec.ts index e5afc67ff..688547287 100644 --- a/discojs/src/models/onnx.spec.ts +++ b/discojs/src/models/onnx.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { List } from "immutable"; import { AutoTokenizer } from "@xenova/transformers"; import { ONNXModel } from "./onnx.js"; -import { DefaultGenerationConfig } from "./gpt/config.js"; +import { DefaultGenerationConfig } from "./implementations/gpt/config.js"; describe("ONNXModel.predict", { timeout: 50_000 }, () => { it("should generate the next token ID from a prompt", async () => { diff --git a/discojs/src/models/onnx.ts b/discojs/src/models/onnx.ts index c2269862a..fc7a17302 100644 --- a/discojs/src/models/onnx.ts +++ b/discojs/src/models/onnx.ts @@ -7,8 +7,8 @@ import { Model } from "./index.js"; import type { WeightsContainer } from "../index.js"; import { List } from "immutable"; import type { CausalLMOutput } from "@xenova/transformers"; -import type { GenerationConfig as TFJSGenerationConfig } from "./gpt/config.js"; -import { DefaultGenerationConfig } from "./gpt/config.js"; +import type { GenerationConfig as TFJSGenerationConfig } from "./implementations/gpt/config.js"; +import { DefaultGenerationConfig } from "./implementations/gpt/config.js"; import type { Batched, DataFormat } from "../index.js"; export class ONNXModel extends Model<"text"> { diff --git a/discojs/src/task/task_provider.ts b/discojs/src/task/task_provider.ts index 939e68120..5f6a150ae 100644 --- a/discojs/src/task/task_provider.ts +++ b/discojs/src/task/task_provider.ts @@ -1,7 +1,8 @@ -import type { DataType, Model, Network, Task } from "../index.js"; +import type { DataType, Model, Network, Task, ModelCard } from "../index.js"; export interface TaskProvider { getTask(): Promise>; // Create the corresponding model ready for training (compiled) getModel(): Promise>; + modelCard: ModelCard; } diff --git a/docs/examples/custom_task.ts b/docs/examples/custom_task.ts index 706c465c5..0c9aa1f08 100644 --- a/docs/examples/custom_task.ts +++ b/docs/examples/custom_task.ts @@ -1,9 +1,41 @@ import tf from "@tensorflow/tfjs-node"; -import type { TaskProvider } from "@epfml/discojs"; +import type { TaskProvider, ModelCard } from "@epfml/discojs"; import { defaultTasks, models } from "@epfml/discojs"; import { Server as DiscoServer } from "server"; +// Define your own model card +const customModelCard: ModelCard<"tabular"> = { + card: { + id: "custom_model_id", + name: "Custom name", + preTrained: false, + }, + + async getModel() { + const model = tf.sequential(); + + model.add( + tf.layers.dense({ + inputShape: [1], + units: 124, + activation: "relu", + kernelInitializer: "leCunNormal", + }), + ); + model.add(tf.layers.dense({ units: 32, activation: "relu" })); + model.add(tf.layers.dense({ units: 1, activation: "sigmoid" })); + + model.compile({ + optimizer: "rmsprop", + loss: "binaryCrossentropy", + metrics: ["accuracy"], + }); + + return Promise.resolve(new models.TFJS("tabular", model)); + }, +}; + // Define your own task provider (task definition + model) const customTask: TaskProvider<"tabular", "federated"> = { getTask() { @@ -55,6 +87,8 @@ const customTask: TaskProvider<"tabular", "federated"> = { return Promise.resolve(new models.TFJS("tabular", model)); }, + + modelCard: customModelCard, }; async function runServer(): Promise { diff --git a/server/tests/e2e/federated.spec.ts b/server/tests/e2e/federated.spec.ts index acd076555..dab15f9d3 100644 --- a/server/tests/e2e/federated.spec.ts +++ b/server/tests/e2e/federated.spec.ts @@ -9,7 +9,7 @@ import type { TaskProvider, WeightsContainer, } from "@epfml/discojs"; -import { Disco, defaultTasks } from "@epfml/discojs"; +import { Disco, defaultTasks, defaultModels } from "@epfml/discojs"; import { List } from "immutable"; import { assert, afterEach, describe, expect, it } from "vitest"; import { Server } from "../../src/index.js"; @@ -85,6 +85,7 @@ describe("end-to-end federated", () => { const url = await startServer({ getModel: () => defaultTasks.cifar10.getModel(), getTask: () => Promise.resolve(cifar10Task), + modelCard: defaultModels.CIFAR10Classifier, }); const dataset = await datasets.loadCifar10(); From 3cd3955aa7ebaf7f7f6f68745a29c7fa29e4f035 Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 5 Aug 2026 18:30:11 +0200 Subject: [PATCH 3/5] refactor: add import aliases and remove some cycles --- .gitignore | 1 + .knip.json | 2 +- Dockerfile | 1 + cli/tsconfig.json | 8 +- discojs-node/package.json | 8 +- discojs-node/tsconfig.lib.json | 10 +- discojs-node/tsconfig.vitest.json | 2 +- discojs-web/package.json | 8 +- discojs-web/tsconfig.lib.json | 7 +- discojs-web/tsconfig.vitest.json | 5 +- discojs/package.json | 37 +- discojs/src/default_tasks/cifar10.ts | 12 +- discojs/src/default_tasks/lus_covid.ts | 10 +- discojs/src/default_tasks/mnist.ts | 10 +- discojs/src/default_tasks/simple_face.ts | 11 +- discojs/src/default_tasks/tinder_dog.ts | 10 +- discojs/src/default_tasks/titanic.ts | 10 +- discojs/src/default_tasks/wikitext.ts | 8 +- discojs/src/index.ts | 2 +- discojs/src/models/cards/LUSClassifier.ts | 5 +- .../src/models/cards/mobileNetV1_cifar10.ts | 4 +- .../implementations/LUSImageClassifier.ts | 3 +- .../src/models/implementations/gpt/index.ts | 8 +- .../models/implementations/hellaswag.spec.ts | 6 +- .../src/models/implementations/hellaswag.ts | 5 +- discojs/src/models/index.ts | 13 +- discojs/src/models/model.ts | 10 +- discojs/src/models/model_card.ts | 3 +- discojs/src/models/onnx.ts | 10 +- discojs/src/models/tfjs.ts | 17 +- discojs/tsconfig.lib.json | 8 +- discojs/tsconfig.vitest.json | 5 +- docs/TASK.md | 2 +- docs/examples/tsconfig.json | 8 +- onnx-converter/package.json | 2 +- onnx-converter/tsconfig.lib.json | 7 +- package.json | 2 +- pnpm-lock.yaml | 459 ++++++++++-------- server/package.json | 8 +- server/tsconfig.lib.json | 12 +- server/tsconfig.vitest.json | 8 - tsconfig.base.json | 1 + tsconfig.base.lib.json | 6 + tsconfig.eslint.json | 2 +- webapp/cypress/tsconfig.json | 4 +- webapp/package.json | 3 +- webapp/tsconfig.app.json | 5 +- webapp/tsconfig.node.json | 9 - webapp/tsconfig.vitest.json | 6 +- 49 files changed, 444 insertions(+), 359 deletions(-) create mode 100644 tsconfig.base.lib.json diff --git a/.gitignore b/.gitignore index 4036c9ec6..bc5279ed2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # stored trained models /server/models/ /models/ +/docs/examples/models/ # tsc built /*/dist/ diff --git a/.knip.json b/.knip.json index f33580dd0..468916b72 100644 --- a/.knip.json +++ b/.knip.json @@ -5,12 +5,12 @@ "ignoreDependencies": ["typescript-eslint", "supports-color"] }, "webapp": { - "ignore": ["env.d.ts"], "ignoreDependencies": ["buffer"] }, "isomorphic-wrtc": {}, "cli": { "entry": [ + "src/cli.ts", "src/benchmark_gpt.ts", "src/hellaswag_gpt.ts", "src/train_gpt.ts" diff --git a/Dockerfile b/Dockerfile index be4ebc5fa..9c9631f70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN pnpm ci COPY discojs/ discojs/ COPY discojs-node/ discojs-node/ COPY tsconfig.base.json . +COPY tsconfig.base.lib.json . RUN pnpm -F discojs -F discojs-node run build COPY server/ server/ diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 0e72dc12f..db9ba0027 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -1,14 +1,14 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.base.lib.json", "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.lib.json" }, { - "path": "../discojs-node" + "path": "../discojs-node/tsconfig.lib.json" }, { - "path": "../server" + "path": "../server/tsconfig.lib.json" } ], "compilerOptions": { diff --git a/discojs-node/package.json b/discojs-node/package.json index 0adf0a4e3..534cf443a 100644 --- a/discojs-node/package.json +++ b/discojs-node/package.json @@ -2,11 +2,15 @@ "name": "@epfml/discojs-node", "version": "3.0.0", "type": "module", - "exports": "./dist/index.js", + "exports": { + "@disco/source": "./src/index.ts", + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, "types": "dist/index.d.ts", "scripts": { "watch": "nodemon --ext ts --ignore dist --watch ../discojs/dist --watch . --exec pnpm run", - "build": "tsc --build", + "build": "tsc --build tsconfig.lib.json", "test": "cd .. && vitest --run --project=discojs-node" }, "repository": { diff --git a/discojs-node/tsconfig.lib.json b/discojs-node/tsconfig.lib.json index d165722cd..c9d66d4a8 100644 --- a/discojs-node/tsconfig.lib.json +++ b/discojs-node/tsconfig.lib.json @@ -1,11 +1,15 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.base.lib.json", "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.lib.json" } ], - "compilerOptions": { "rootDir": "./src", "outDir": "dist" }, + "compilerOptions": { + "rootDir": "./src", + "outDir": "dist", + "composite": true + }, "include": ["src"], "exclude": ["**/*.spec.ts"] } diff --git a/discojs-node/tsconfig.vitest.json b/discojs-node/tsconfig.vitest.json index 8791b4e98..48ca20af3 100644 --- a/discojs-node/tsconfig.vitest.json +++ b/discojs-node/tsconfig.vitest.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.base.json", "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.vitest.json" } ], "compilerOptions": { "noEmit": true }, diff --git a/discojs-web/package.json b/discojs-web/package.json index f29eac08f..b1690788e 100644 --- a/discojs-web/package.json +++ b/discojs-web/package.json @@ -2,11 +2,15 @@ "name": "@epfml/discojs-web", "version": "3.0.0", "type": "module", - "exports": "./dist/index.js", + "exports": { + "@disco/source": "./src/index.ts", + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, "types": "dist/index.d.ts", "scripts": { "watch": "nodemon --ext ts --ignore dist --watch ../discojs/dist --watch . --exec pnpm run", - "build": "tsc --build", + "build": "tsc --build tsconfig.lib.json", "test": "cd .. && vitest --run --project=discojs-web" }, "repository": { diff --git a/discojs-web/tsconfig.lib.json b/discojs-web/tsconfig.lib.json index c8c3aae4a..b08d8536a 100644 --- a/discojs-web/tsconfig.lib.json +++ b/discojs-web/tsconfig.lib.json @@ -1,14 +1,15 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.base.lib.json", "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.lib.json" } ], "compilerOptions": { "lib": ["DOM"], "rootDir": "./src", - "outDir": "dist" + "outDir": "dist", + "composite": true }, "include": ["src"], "exclude": ["**/*.spec.ts"] diff --git a/discojs-web/tsconfig.vitest.json b/discojs-web/tsconfig.vitest.json index 781b7323a..bfc451458 100644 --- a/discojs-web/tsconfig.vitest.json +++ b/discojs-web/tsconfig.vitest.json @@ -2,12 +2,13 @@ "extends": "../tsconfig.base.json", "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.vitest.json" } ], "compilerOptions": { "lib": ["DOM"], - "noEmit": true + "noEmit": true, + "composite": true }, "include": ["src"] } diff --git a/discojs/package.json b/discojs/package.json index aac3ca8d6..30957ab9c 100644 --- a/discojs/package.json +++ b/discojs/package.json @@ -2,11 +2,17 @@ "name": "@epfml/discojs", "version": "3.0.0", "type": "module", - "exports": "./dist/index.js", + "exports": { + ".": { + "@disco/source": "./src/index.ts", + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, "types": "dist/index.d.ts", "scripts": { "watch": "nodemon --ext ts --ignore dist --exec pnpm run", - "build": "tsc --build", + "build": "tsc --build tsconfig.lib.json", "test": "cd .. && vitest --run --project=discojs" }, "repository": { @@ -16,6 +22,33 @@ "bugs": { "url": "https://github.com/epfml/disco/issues" }, + "imports": { + "#models/*": { + "@disco/source": "./src/models/*.ts", + "default": "./dist/models/*.js", + "types": "./dist/models/*.d.ts" + }, + "#task/*": { + "@disco/source": "./src/task/*.ts", + "default": "./dist/task/*.js", + "types": "./dist/task/*.d.ts" + }, + "#weights/*": { + "@disco/source": "./src/weights/*.ts", + "default": "./dist/weights/*.js", + "types": "./dist/weights/*.d.ts" + }, + "#dtypes/*": { + "@disco/source": "./src/types/*.ts", + "default": "./dist/types/*.js", + "types": "./dist/types/*.d.ts" + }, + "#dataset/*": { + "@disco/source": "./src/dataset/*.ts", + "default": "./dist/dataset/*.js", + "types": "./dist/dataset/*.d.ts" + } + }, "homepage": "https://github.com/epfml/disco#readme", "dependencies": { "@epfml/isomorphic-wrtc": "workspace:*", diff --git a/discojs/src/default_tasks/cifar10.ts b/discojs/src/default_tasks/cifar10.ts index 05f34d555..c5f95c1dc 100644 --- a/discojs/src/default_tasks/cifar10.ts +++ b/discojs/src/default_tasks/cifar10.ts @@ -1,9 +1,5 @@ -import * as tf from "@tensorflow/tfjs"; - -import type { Model, TaskProvider } from "../index.js"; -import { models } from "../index.js"; - -import baseModel from "../models/implementations/mobileNet_v1_025_224.js"; +import type { TaskProvider } from "#task/index"; +import { cards } from "#models/index"; export const cifar10: TaskProvider<"image", "decentralized"> = { getTask() { @@ -89,8 +85,8 @@ export const cifar10: TaskProvider<"image", "decentralized"> = { metrics: ["accuracy"], }); - return new models.TFJS("image", model); + return new TFJS("image", model); }, - modelCard: models.cards.CIFAR10Classifier, + modelCard: cards.CIFAR10Classifier, }; diff --git a/discojs/src/default_tasks/lus_covid.ts b/discojs/src/default_tasks/lus_covid.ts index 5787317c0..c55f7d20b 100644 --- a/discojs/src/default_tasks/lus_covid.ts +++ b/discojs/src/default_tasks/lus_covid.ts @@ -1,7 +1,5 @@ -import * as tf from "@tensorflow/tfjs"; - -import type { Model, TaskProvider } from "../index.js"; -import { models } from "../index.js"; +import type { TaskProvider } from "#task/index"; +import { cards } from "#models/index"; export const lusCovid: TaskProvider<"image", "federated"> = { getTask() { @@ -105,8 +103,8 @@ export const lusCovid: TaskProvider<"image", "federated"> = { metrics: ["accuracy"], }); - return Promise.resolve(new models.TFJS("image", model)); + return Promise.resolve(new TFJS("image", model)); }, - modelCard: models.cards.LUSClassifier, + modelCard: cards.LUSClassifier, }; diff --git a/discojs/src/default_tasks/mnist.ts b/discojs/src/default_tasks/mnist.ts index f15f73d1a..b9bb94c00 100644 --- a/discojs/src/default_tasks/mnist.ts +++ b/discojs/src/default_tasks/mnist.ts @@ -1,7 +1,5 @@ -import * as tf from "@tensorflow/tfjs"; - -import type { Model, TaskProvider } from "../index.js"; -import { models } from "../index.js"; +import type { TaskProvider } from "#task/index"; +import { cards } from "#models/index"; export const mnist: TaskProvider<"image", "decentralized"> = { getTask() { @@ -82,8 +80,8 @@ export const mnist: TaskProvider<"image", "decentralized"> = { metrics: ["accuracy"], }); - return Promise.resolve(new models.TFJS("image", model)); + return Promise.resolve(new TFJS("image", model)); }, - modelCard: models.cards.PytorchMNISTClassifier, + modelCard: cards.PytorchMNISTClassifier, }; diff --git a/discojs/src/default_tasks/simple_face.ts b/discojs/src/default_tasks/simple_face.ts index a1bc77564..120b3bfc9 100644 --- a/discojs/src/default_tasks/simple_face.ts +++ b/discojs/src/default_tasks/simple_face.ts @@ -1,8 +1,5 @@ -import * as tf from "@tensorflow/tfjs"; - -import type { Model, TaskProvider } from "../index.js"; -import { models } from "../index.js"; -import baseModel from "../models/implementations/mobileNetV2_35_alpha_2_classes.js"; +import type { TaskProvider } from "#task/index"; +import { cards } from "#models/index"; export const simpleFace: TaskProvider<"image", "federated"> = { getTask() { @@ -53,8 +50,8 @@ export const simpleFace: TaskProvider<"image", "federated"> = { metrics: ["accuracy"], }); - return new models.TFJS("image", model); + return new TFJS("image", model); }, - modelCard: models.cards.FaceClassifier, + modelCard: cards.FaceClassifier, }; diff --git a/discojs/src/default_tasks/tinder_dog.ts b/discojs/src/default_tasks/tinder_dog.ts index b433a0857..c728c6f3b 100644 --- a/discojs/src/default_tasks/tinder_dog.ts +++ b/discojs/src/default_tasks/tinder_dog.ts @@ -1,7 +1,5 @@ -import * as tf from "@tensorflow/tfjs"; - -import type { Model, TaskProvider } from "../index.js"; -import { models } from "../index.js"; +import type { TaskProvider } from "#task/index"; +import { cards } from "#models/index"; export const tinderDog: TaskProvider<"image", "federated"> = { getTask() { @@ -95,8 +93,8 @@ export const tinderDog: TaskProvider<"image", "federated"> = { metrics: ["accuracy"], }); - return Promise.resolve(new models.TFJS("image", model)); + return Promise.resolve(new TFJS("image", model)); }, - modelCard: models.cards.DogClassifier, + modelCard: cards.DogClassifier, }; diff --git a/discojs/src/default_tasks/titanic.ts b/discojs/src/default_tasks/titanic.ts index ece837957..a038d2abe 100644 --- a/discojs/src/default_tasks/titanic.ts +++ b/discojs/src/default_tasks/titanic.ts @@ -1,7 +1,5 @@ -import * as tf from "@tensorflow/tfjs"; - -import type { Model, TaskProvider } from "../index.js"; -import { models } from "../index.js"; +import type { TaskProvider } from "#task/index"; +import { cards } from "#models/index"; export const titanic: TaskProvider<"tabular", "federated"> = { getTask() { @@ -76,8 +74,8 @@ export const titanic: TaskProvider<"tabular", "federated"> = { metrics: ["accuracy"], }); - return Promise.resolve(new models.TFJS("tabular", model)); + return Promise.resolve(new TFJS("tabular", model)); }, - modelCard: models.cards.TitanicClassifier, + modelCard: cards.TitanicClassifier, }; diff --git a/discojs/src/default_tasks/wikitext.ts b/discojs/src/default_tasks/wikitext.ts index c87e6b9fa..2f73b645c 100644 --- a/discojs/src/default_tasks/wikitext.ts +++ b/discojs/src/default_tasks/wikitext.ts @@ -1,5 +1,5 @@ -import type { TaskProvider } from "../index.js"; -import { Tokenizer, models } from "../index.js"; +import type { TaskProvider } from "#task/index"; +import { Tokenizer, cards } from "#models/index"; export const wikitext: TaskProvider<"text", "federated"> = { async getTask() { @@ -51,10 +51,10 @@ export const wikitext: TaskProvider<"text", "federated"> = { async getModel() { const task = await this.getTask(); - return new models.GPT({ + return new GPT({ contextLength: task.trainingInformation.contextLength, }); }, - modelCard: models.cards.Wikitext, + modelCard: cards.Wikitext, }; diff --git a/discojs/src/index.ts b/discojs/src/index.ts index 77baf09a8..4ae4782ce 100644 --- a/discojs/src/index.ts +++ b/discojs/src/index.ts @@ -18,12 +18,12 @@ export { Validator } from "./validator.js"; export { Model, + ModelCard, BatchLogs, EpochLogs, Tokenizer, ValidationMetrics, } from "./models/index.js"; -export { ModelCard } from "./models/index.js"; export * as models from "./models/index.js"; export * from "./task/index.js"; diff --git a/discojs/src/models/cards/LUSClassifier.ts b/discojs/src/models/cards/LUSClassifier.ts index cd24caef1..ba107350e 100644 --- a/discojs/src/models/cards/LUSClassifier.ts +++ b/discojs/src/models/cards/LUSClassifier.ts @@ -1,5 +1,6 @@ -import { Model, ModelCard } from "../index.js"; -import { model } from "../implementations/LUSImageClassifier.js"; +import { Model } from "#models/model"; +import { ModelCard } from "#models/model_card"; +import { model } from "#models/implementations/LUSImageClassifier"; export const LUSClassifier: ModelCard<"image"> = { card: { diff --git a/discojs/src/models/cards/mobileNetV1_cifar10.ts b/discojs/src/models/cards/mobileNetV1_cifar10.ts index 56458c1e8..cb2980a2d 100644 --- a/discojs/src/models/cards/mobileNetV1_cifar10.ts +++ b/discojs/src/models/cards/mobileNetV1_cifar10.ts @@ -1,5 +1,5 @@ -import { Model, ModelCard } from "../index.js"; -import { getModel } from "../implementations/mobileNetV1_cifar10.js"; +import { Model, ModelCard } from "#models/index"; +import { getModel } from "#models/implementations/mobileNetV1_cifar10"; export const CIFAR10Classifier: ModelCard<"image"> = { card: { diff --git a/discojs/src/models/implementations/LUSImageClassifier.ts b/discojs/src/models/implementations/LUSImageClassifier.ts index f2e480697..96174b58b 100644 --- a/discojs/src/models/implementations/LUSImageClassifier.ts +++ b/discojs/src/models/implementations/LUSImageClassifier.ts @@ -1,6 +1,7 @@ import * as tf from "@tensorflow/tfjs"; -import { Model, TFJS } from "../index.js"; +import { Model } from "#models/model"; +import { TFJS } from "#models/tfjs"; // Model architecture from tensorflow.js docs: // https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/index.html#4 diff --git a/discojs/src/models/implementations/gpt/index.ts b/discojs/src/models/implementations/gpt/index.ts index ad13e4b73..2be25eaf2 100644 --- a/discojs/src/models/implementations/gpt/index.ts +++ b/discojs/src/models/implementations/gpt/index.ts @@ -7,10 +7,10 @@ import createDebug from "debug"; import { List, Range } from "immutable"; import * as tf from "@tensorflow/tfjs"; -import type { Batched, Dataset, DataFormat } from "../../../index.js"; -import { WeightsContainer } from "../../../index.js"; - -import { BatchLogs, Model, EpochLogs } from "../../index.js"; +import { WeightsContainer } from "#weights/index"; +import { Dataset, Batched } from "#dataset/index"; +import { Model, BatchLogs, EpochLogs } from "#models/index"; +import type { DataFormat } from "#dtypes/index"; import { GPTModel } from "./model.js"; import evaluate from "./evaluate.js"; diff --git a/discojs/src/models/implementations/hellaswag.spec.ts b/discojs/src/models/implementations/hellaswag.spec.ts index b5daab828..e8798f912 100644 --- a/discojs/src/models/implementations/hellaswag.spec.ts +++ b/discojs/src/models/implementations/hellaswag.spec.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { evaluate } from "./hellaswag.js"; -import { GPT, Tokenizer } from "../index.js"; -import { ONNXModel } from "../onnx.js"; import type { HellaSwagExample } from "./hellaswag.js"; +import { evaluate } from "./hellaswag.js"; +import { GPT, Tokenizer } from "#models/index"; +import { ONNXModel } from "#models/onnx"; const exampleDataset: HellaSwagExample[] = [ { diff --git a/discojs/src/models/implementations/hellaswag.ts b/discojs/src/models/implementations/hellaswag.ts index b1447fe3a..b4c643fa3 100644 --- a/discojs/src/models/implementations/hellaswag.ts +++ b/discojs/src/models/implementations/hellaswag.ts @@ -1,8 +1,7 @@ import * as tf from "@tensorflow/tfjs"; import { List } from "immutable"; -import type { Tokenizer } from "../index.js"; -import { GPT } from "../index.js"; -import type { ONNXModel } from "../onnx.js"; +import type { Tokenizer, ONNXModel } from "#models/index"; +import { GPT } from "#models/index"; export const HELLASWAG_URL = "https://raw.githubusercontent.com/rowanz/hellaswag/master/data/hellaswag_val.jsonl"; diff --git a/discojs/src/models/index.ts b/discojs/src/models/index.ts index 40441c4ca..96406e2dc 100644 --- a/discojs/src/models/index.ts +++ b/discojs/src/models/index.ts @@ -1,17 +1,20 @@ export { Model } from "./model.js"; -export { BatchLogs, EpochLogs, ValidationMetrics } from "./logs.js"; +export type { BatchLogs, ValidationMetrics } from "./logs.js"; +export { EpochLogs } from "./logs.js"; export { Tokenizer } from "./tokenizer.js"; export { GPT } from "./implementations/gpt/index.js"; export { ONNXModel } from "./onnx.js"; -export { GPTConfig } from "./implementations/gpt/config.js"; -export { - evaluate as evaluate_hellaswag, +export type { GPTConfig } from "./implementations/gpt/config.js"; +export type { HellaSwagDataset, HellaSwagExample, +} from "./implementations/hellaswag.js"; +export { + evaluate as evaluate_hellaswag, HELLASWAG_URL, } from "./implementations/hellaswag.js"; export { TFJS } from "./tfjs.js"; -export { ModelCard } from "./model_card.js"; +export type { ModelCard } from "./model_card.js"; export * as cards from "./cards/index.js"; diff --git a/discojs/src/models/model.ts b/discojs/src/models/model.ts index 47cc7e857..20ba25d84 100644 --- a/discojs/src/models/model.ts +++ b/discojs/src/models/model.ts @@ -1,10 +1,6 @@ -import type { - Batched, - Dataset, - DataFormat, - DataType, - WeightsContainer, -} from "../index.js"; +import { WeightsContainer } from "#weights/index"; +import { Dataset, Batched } from "#dataset/index"; +import type { DataFormat, DataType } from "#dtypes/index"; import type { BatchLogs, EpochLogs, ValidationMetrics } from "./logs.js"; diff --git a/discojs/src/models/model_card.ts b/discojs/src/models/model_card.ts index 2996e99f2..45f913528 100644 --- a/discojs/src/models/model_card.ts +++ b/discojs/src/models/model_card.ts @@ -1,5 +1,6 @@ import { z } from "zod"; -import type { DataType, Model } from "../index.js"; +import type { Model } from "#models/index"; +import type { DataType } from "#dtypes/index"; namespace ModelCardInfo { export type ID = string; diff --git a/discojs/src/models/onnx.ts b/discojs/src/models/onnx.ts index fc7a17302..f99418673 100644 --- a/discojs/src/models/onnx.ts +++ b/discojs/src/models/onnx.ts @@ -1,15 +1,17 @@ +import type { CausalLMOutput } from "@xenova/transformers"; import { AutoModelForCausalLM, PreTrainedModel, Tensor, } from "@xenova/transformers"; -import { Model } from "./index.js"; -import type { WeightsContainer } from "../index.js"; import { List } from "immutable"; -import type { CausalLMOutput } from "@xenova/transformers"; + +import type { WeightsContainer } from "#weights/index"; +import type { Batched } from "#dataset/index"; +import type { DataFormat } from "#dtypes/index"; import type { GenerationConfig as TFJSGenerationConfig } from "./implementations/gpt/config.js"; +import { Model } from "#models/model"; import { DefaultGenerationConfig } from "./implementations/gpt/config.js"; -import type { Batched, DataFormat } from "../index.js"; export class ONNXModel extends Model<"text"> { private model: PreTrainedModel; diff --git a/discojs/src/models/tfjs.ts b/discojs/src/models/tfjs.ts index 0254d3be7..ed88b0a24 100644 --- a/discojs/src/models/tfjs.ts +++ b/discojs/src/models/tfjs.ts @@ -1,17 +1,12 @@ import { List, Map, Range } from "immutable"; import * as tf from "@tensorflow/tfjs"; -import { - Batched, - Dataset, - DataFormat, - DataType, - WeightsContainer, -} from "../index.js"; - -import { BatchLogs } from "./index.js"; -import { Model } from "./index.js"; -import { EpochLogs } from "./logs.js"; +import { WeightsContainer } from "#weights/index"; +import { Dataset, Batched } from "#dataset/index"; +import type { DataFormat, DataType } from "#dtypes/index"; + +import { Model } from "./model.js"; +import { BatchLogs, EpochLogs } from "./logs.js"; type Serialized = [D, tf.io.ModelArtifacts]; diff --git a/discojs/tsconfig.lib.json b/discojs/tsconfig.lib.json index 805f1a75d..90ec2cefa 100644 --- a/discojs/tsconfig.lib.json +++ b/discojs/tsconfig.lib.json @@ -1,6 +1,10 @@ { - "extends": "../tsconfig.base.json", - "compilerOptions": { "rootDir": "./src", "outDir": "dist" }, + "extends": "../tsconfig.base.lib.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "dist", + "composite": true + }, "include": ["src"], "exclude": ["**/*.spec.ts"] } diff --git a/discojs/tsconfig.vitest.json b/discojs/tsconfig.vitest.json index 63288c889..7060e216b 100644 --- a/discojs/tsconfig.vitest.json +++ b/discojs/tsconfig.vitest.json @@ -1,5 +1,8 @@ { "extends": "../tsconfig.base.json", - "compilerOptions": { "noEmit": true }, + "compilerOptions": { + "noEmit": true, + "composite": true + }, "include": ["src"] } diff --git a/docs/TASK.md b/docs/TASK.md index 5ea0fca3f..8f214a5ef 100644 --- a/docs/TASK.md +++ b/docs/TASK.md @@ -6,7 +6,7 @@ DISCO currently allows learning of arbitrary machine learning tasks, where tasks 2. **Task creation UI**: new tasks can be defined via the [**task creation form**](https://discolab.ai/#/create) 3. **Implementing custom tasks**: tasks too specific for the UI form need to be implemented in the repository directly. -In any case, one user needs to upload the initial model that is going to be trained collaboratively. +In either case, one user needs to upload the initial model that is going to be trained collaboratively. ### Uploading ML models diff --git a/docs/examples/tsconfig.json b/docs/examples/tsconfig.json index a25439787..e1d75b158 100644 --- a/docs/examples/tsconfig.json +++ b/docs/examples/tsconfig.json @@ -1,14 +1,14 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.base.lib.json", "references": [ { - "path": "../../discojs" + "path": "../../discojs/tsconfig.lib.json" }, { - "path": "../../discojs-node" + "path": "../../discojs-node/tsconfig.lib.json" }, { - "path": "../../server" + "path": "../../server/tsconfig.lib.json" } ], "compilerOptions": { diff --git a/onnx-converter/package.json b/onnx-converter/package.json index 9bd9679d0..17d6eeb1a 100644 --- a/onnx-converter/package.json +++ b/onnx-converter/package.json @@ -6,7 +6,7 @@ "scripts": { "convert_onnx": "pnpm run build && node dist/convert_onnx.js", "watch": "nodemon --ext ts --ignore dist --exec pnpm run", - "build": "tsc --build && cp -r src/protobuf dist", + "build": "tsc --build tsconfig.lib.json && cp -r src/protobuf dist", "lint": "pnpm exec eslint .", "test": ": nothing" }, diff --git a/onnx-converter/tsconfig.lib.json b/onnx-converter/tsconfig.lib.json index aeebf2596..1aadc7641 100644 --- a/onnx-converter/tsconfig.lib.json +++ b/onnx-converter/tsconfig.lib.json @@ -1,5 +1,10 @@ { - "extends": "../tsconfig.base.json", + "extends": "../tsconfig.base.lib.json", + "references": [ + { + "path": "../discojs/tsconfig.lib.json" + } + ], "compilerOptions": { "rootDir": "./src", "outDir": "dist", diff --git a/package.json b/package.json index bf711b225..2c0fdc52c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "eslint": "10.0.3", "eslint-plugin-cypress": "6.2.0", "eslint-plugin-vue": "10.9.2", - "knip": "6.17.1", + "knip": "6.31.0", "prettier": "3.6.2", "supports-color": "8.1.1", "typescript": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6c24d297..363d1a0b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,8 +260,8 @@ importers: specifier: 10.9.2 version: 10.9.2(@typescript-eslint/parser@8.62.1(eslint@10.0.3(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.0.3(jiti@2.7.0)(supports-color@8.1.1))(vue-eslint-parser@10.4.1(eslint@10.0.3(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)) knip: - specifier: 6.17.1 - version: 6.17.1 + specifier: 6.31.0 + version: 6.31.0 prettier: specifier: 3.6.2 version: 3.6.2 @@ -639,8 +639,8 @@ importers: specifier: 'catalog:' version: 4.1.9(@types/node@22.19.15)(jsdom@29.0.1(@noble/hashes@1.8.0)(canvas@3.2.3))(vite@8.0.16(@types/node@22.19.15)(jiti@2.7.0)(yaml@2.9.0)) vue-tsc: - specifier: 3.2.6 - version: 3.2.6(typescript@6.0.3) + specifier: 3.3.9 + version: 3.3.9(typescript@6.0.3) vue3-spinners: specifier: 1.3.3 version: 1.3.3(vue@3.5.34(typescript@6.0.3)) @@ -753,15 +753,24 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1079,129 +1088,129 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - '@oxc-parser/binding-android-arm-eabi@0.135.0': - resolution: {integrity: sha512-sHeZItACNcA5WRAWqF6ixriR4GkZDyY10gVgnZU7pXku1DjHFATSqnwZM809jl0gXPHxb6fKzYQCK7bNK5cACQ==} + '@oxc-parser/binding-android-arm-eabi@0.142.0': + resolution: {integrity: sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm64@0.135.0': - resolution: {integrity: sha512-wPte+SzgzWWFgMSF8YZDNM+tBXtJg0AXBi7+tU3yS2z1f2Af9kRLZLKuJojADmuD/cZexmnMHHC3SDItTW77Iw==} + '@oxc-parser/binding-android-arm64@0.142.0': + resolution: {integrity: sha512-WZkvGRLNQTz8lR9zP5nLjUdlroRCopBu3g9zF1p/laE6DzT1UbQo8Rdz5MWhaJUPYg/6gp+jo7HUgsyKaN1FtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.135.0': - resolution: {integrity: sha512-BmKz3lHIsqVos+9aPcdYCT9MG3APoUyM43KlEFhJMWNVDOGG8FKyiFz81Bc+mGz2o0hpuQ3PfXLfVWJrKXjo2g==} + '@oxc-parser/binding-darwin-arm64@0.142.0': + resolution: {integrity: sha512-l4khS8LQOOVYsGRVARo1gSaCT/aBSceUVXgtovWc2+drnxVuDr082WA3OCHVdVzIz5JIrP/y9CWsSKxBDNmYGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.135.0': - resolution: {integrity: sha512-dM8BS+8+Br1fNvmh2QZbGiHaYttwLebRa6J4Uz9vuFzMNmvsdRYwf7993ptOaV0JTrR63AaoVLjX7nhWbijxjQ==} + '@oxc-parser/binding-darwin-x64@0.142.0': + resolution: {integrity: sha512-QBsNF3nqlXmcH2B1YOPqQYmCJoy4HuIjUxGbBO/k5JAJUl68ghU2psRY2zPk+RyBaWqKP/qfL4oaFgEMCdwskA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.135.0': - resolution: {integrity: sha512-xlZnvvJdR9bGu2pOhvR5hMuKPHCE6Sa9owK5A484mzjHdm75VRV5nCs5w/jkmGODMMTFc+KN7EnZqEieM813kw==} + '@oxc-parser/binding-freebsd-x64@0.142.0': + resolution: {integrity: sha512-b7Q7m4Cqc6XqNhri3R+QhU+GVy646Pn+bkdhrDdWym/Fdi0ZUa+d73H9dm5H91JtbtAQ/z1d8XKMW3oOV8a4tQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.135.0': - resolution: {integrity: sha512-PSR8LmBK/H/PQRiN8g7RebQgZX/ntVCrdT/JBfNxE5ezdHG1s2i4rbazsRJYD83TTI1MmgTpC0MGL42PLtskQQ==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.142.0': + resolution: {integrity: sha512-3riVS5IhdH3uCZj1Y9ftDQlR0dvLsIlw/edrRqk8JhgNd5K0XSs+UBtgh50N13CAlW9/TXj6sVGXaKNBocd0Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.135.0': - resolution: {integrity: sha512-I85GJXzfUsigkkk7Ngdz95C217M4FdUi1Z2HrX5UyPmURobwQZ7m2bbUvwFkz4VGZd+lymFGKHvDZ3RQC9qOzA==} + '@oxc-parser/binding-linux-arm-musleabihf@0.142.0': + resolution: {integrity: sha512-NmXUOpgpTSkhl795TiXmWppTwmSJ92RC1qvD6e4XOF+slgmo3e6Ah+kEu+6AN8s7NAOEwqGmir58MgSQSWmBSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.135.0': - resolution: {integrity: sha512-zqEY0npz0g0aGZj/8a5BclunjVDytsBQHYtIC10Gd26HcrLwbVF6YDbqRQjunMGYdSo97u6xOBl05aTDI2diDQ==} + '@oxc-parser/binding-linux-arm64-gnu@0.142.0': + resolution: {integrity: sha512-gc0EXsKtXgerujmU2Bql3u1L1HsSQ2774R83idq/FoNMPVV/RY/1ErFsvnit7KoiP/sLvzQixeUo4Ut0ic0wmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-musl@0.135.0': - resolution: {integrity: sha512-mWAfprP819gQ2qYst1RxgTI8b/z0b29OpoKfRflIXLHde2dZLihQD4g47Onuvtpo5GPIkMYPRlX9QoeZfs/GnQ==} + '@oxc-parser/binding-linux-arm64-musl@0.142.0': + resolution: {integrity: sha512-F2XvmWSE0uWpie+jHKKIFgdVOe9ypGhkEZxKx5DuW215K6cbAC274yYaPkcM7EqY4Df3Weyhpcz3lsURyH2LVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-ppc64-gnu@0.135.0': - resolution: {integrity: sha512-gri8c2AOmJKJwOux2KTHFBfUaXoJURuVMKhmKEi/2hTF55cQteTDV2XNfTiE5oCC+Tnem1Y4/MWzcyDadtsSag==} + '@oxc-parser/binding-linux-ppc64-gnu@0.142.0': + resolution: {integrity: sha512-wLMbT21U/QxknQsk+VvNF0b9D2/aGWhcaQQQ+VYlE8FwD5+GoWZIPPXNzyHmkYyhm0KB3itL+TBavjMatqNnYA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.135.0': - resolution: {integrity: sha512-Y2tkupCG5wo0SxH2rMLG4d4Kmv6DaM3sBp+GuM5lox0S8Za6VxKgQrY2Mut088QQxKkEE89n/4CCCgmw2o0e3Q==} + '@oxc-parser/binding-linux-riscv64-gnu@0.142.0': + resolution: {integrity: sha512-+G8F/4ckwT7FCJV4H2bt09xEzJbjNCfuL4Sp1AYNaFtFMVtgIGMuJlteT82U+K0UIZ/DzAR/LDlMFnEuajG7Kw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-musl@0.135.0': - resolution: {integrity: sha512-xDRJq6i6WTynjeP+ISbDpyH4p9BaJ0wuQcL0lCSDkt9qOXC9dmwpOu1VG/TlwmPI3KpYntmO9nJCuc3TMTsNBA==} + '@oxc-parser/binding-linux-riscv64-musl@0.142.0': + resolution: {integrity: sha512-hTsHtTLxMAfCo+rpF5K3qZJKW2NpPN/CHd4mYB3y7XlSdspHkd2gehDIofP64AacA9nWQw2tY3O7wR6UY8IVOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-parser/binding-linux-s390x-gnu@0.135.0': - resolution: {integrity: sha512-V4MoUuiCRNvihxhIufRxvK+ka013V4joTSK0FAGA1KEjLuNprfH6N/Qw2uxQEVIFuNYMhD/hV6xJ/ptbzlKdHg==} + '@oxc-parser/binding-linux-s390x-gnu@0.142.0': + resolution: {integrity: sha512-6y7qYY3TCUDYjqswImdTGl92y+KA/80twALegQPN27kfY+bG7Ib1+L3jbmrCZQx6wrVnai9IPsEZp07I0hx7JQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.135.0': - resolution: {integrity: sha512-JCFZ7zM7KXOKoPAbK/ZB4wY0M1jxRECiem2UQuiXLjzGqS9+hno7mtX+qyK2F7HWK2xPhyJb+frpcOtk5DKOtg==} + '@oxc-parser/binding-linux-x64-gnu@0.142.0': + resolution: {integrity: sha512-i69kAWU+2LgoH5bR+zWiiu+UzAw7Oxkwv7COeJTeY19pn4e70nKQcr9Pm6cL2Z0Z54d+gl9qADlK/0yyuCPiBA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-musl@0.135.0': - resolution: {integrity: sha512-9jSVS1b3hOV7sdKH4aA2DFfnTz0RgQd0v2BefR+LYbH8yIlmSM22JJZbAAjVeVXmFgUAk3zJQ1tpE/Nd+Vi2YQ==} + '@oxc-parser/binding-linux-x64-musl@0.142.0': + resolution: {integrity: sha512-4SQs678MmjYVrmhAgCWD4o0vpaFszXw9xLX5p2Z9MMFcltxiLkA88wQjh80YHjPrXtpyZ2CWI5m+1yNKM0m2Pw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-parser/binding-openharmony-arm64@0.135.0': - resolution: {integrity: sha512-M857ZLBSdn1Uy/SJJz5zh0qGu67B4P9omCgXGBU2LLqTzraX6ZjVNaKq5yW1PDw/LgJXDXR/dbZfgmB310f11Q==} + '@oxc-parser/binding-openharmony-arm64@0.142.0': + resolution: {integrity: sha512-YHpx9N7Ln3a++Tc8rv+H7mrK1zyJQOAwCFg8LZ3lTs1T5afGWeZrLPhPT9HLnIwSjCyJqPWVMIrMxbjcmBr2oQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-wasm32-wasi@0.135.0': - resolution: {integrity: sha512-2w6DVcntQZX9U5RhXtgiWb3FLWFB5EcwI1U8yr3htOCJUJjagN4BFUHz/Y/d9ZsumndZ6ByxxWEtbUZNE1bfFw==} + '@oxc-parser/binding-wasm32-wasi@0.142.0': + resolution: {integrity: sha512-3pLDyY3+oogW73RM5uehNgAiR/Xfb7fvO2Q1Z1gIqZ2+50XDVQmBVlRkHXZTU4gKnQHpwETNsYQVsJ3joVB2iA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@oxc-parser/binding-win32-arm64-msvc@0.135.0': - resolution: {integrity: sha512-rX1U8+IH2Z37EJjDXKa1iifvUQAdba+vZ4Ewj1iaG5eA/QaSybzclCOwtWa0/5BuUQnnK/T2JHUEFrwhL6Ck2Q==} + '@oxc-parser/binding-win32-arm64-msvc@0.142.0': + resolution: {integrity: sha512-Had/VeVY28Oyb0K+Q4FV8KCzoBycIh93oDK6pCbya9lkzdq+ikMHMgBubsdqqlybjJmQRawCQRrnBRHyQwYvcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.135.0': - resolution: {integrity: sha512-9FAisBbH1QICGAjlJobiuKGd/jOuVmyqniWdQMwTa5SkCl6hhuotBCJf1n46B0flYbSOR5TzfV9HZCWSyb3c/Q==} + '@oxc-parser/binding-win32-ia32-msvc@0.142.0': + resolution: {integrity: sha512-GGi3+YphVHavvgs6gum2UXoNCqzHAmPt/nXkn8ZQZstV2Q1qZD1Mn8fz/nWrDkefHQtrG/+1/XrbMxsBTo6Svw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.135.0': - resolution: {integrity: sha512-wYF+A2AzJ2n7ul6q+Z2G/ia0S2+8cUp0AgWZzoFvF4WmUcl1P7p+o6se1Gdr5wGnWuF0iAMIkGddrjCarNr2yA==} + '@oxc-parser/binding-win32-x64-msvc@0.142.0': + resolution: {integrity: sha512-Ny/Wv4Us1LGC/ljwNTp+Hx3r/pH15EFfeDF0p+n898gt+TtRd6C9SccHcuUhDiNTb8s5tt7jdeAMDRQZ4Vq6hg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1209,109 +1218,109 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - '@oxc-project/types@0.135.0': - resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} - '@oxc-resolver/binding-android-arm-eabi@11.20.0': - resolution: {integrity: sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} cpu: [arm] os: [android] - '@oxc-resolver/binding-android-arm64@11.20.0': - resolution: {integrity: sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q==} + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} cpu: [arm64] os: [android] - '@oxc-resolver/binding-darwin-arm64@11.20.0': - resolution: {integrity: sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ==} + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.20.0': - resolution: {integrity: sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg==} + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.20.0': - resolution: {integrity: sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ==} + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0': - resolution: {integrity: sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg==} + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm-musleabihf@11.20.0': - resolution: {integrity: sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg==} + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.20.0': - resolution: {integrity: sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==} + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-arm64-musl@11.20.0': - resolution: {integrity: sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==} + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': - resolution: {integrity: sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==} + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': - resolution: {integrity: sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==} + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': - resolution: {integrity: sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==} + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': - resolution: {integrity: sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==} + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-x64-gnu@11.20.0': - resolution: {integrity: sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==} + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-x64-musl@11.20.0': - resolution: {integrity: sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==} + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} cpu: [x64] os: [linux] libc: [musl] - '@oxc-resolver/binding-openharmony-arm64@11.20.0': - resolution: {integrity: sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==} + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} cpu: [arm64] os: [openharmony] - '@oxc-resolver/binding-wasm32-wasi@11.20.0': - resolution: {integrity: sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg==} + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': - resolution: {integrity: sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA==} + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.20.0': - resolution: {integrity: sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw==} + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} cpu: [x64] os: [win32] @@ -2021,8 +2030,8 @@ packages: typescript: optional: true - '@vue/language-core@3.2.6': - resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==} + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} '@vue/reactivity@3.5.34': resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} @@ -3407,8 +3416,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - knip@6.17.1: - resolution: {integrity: sha512-HcQsZSQ4Ymhuay4BVzJtM5pFZNDSomYYqcNCZOSITPQh9g18a09DqziWAxSt2G+BH9wGlG+0ZjWpEnaFlnKseQ==} + knip@6.31.0: + resolution: {integrity: sha512-NbeIEmUS2VUMjAkbiSNOKPJeV9wpCsr0660sUyKyMQbk4Iom0++nTLInVp4MJ+LfR4kORnw67bDi5tvO7YLnzA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3778,12 +3787,12 @@ packages: outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} - oxc-parser@0.135.0: - resolution: {integrity: sha512-/DaPStu0s2zzNSRRniKyTPM6Z/o+DapOp2JYNKDL8AsgaBGPK2IdZyB87SQjVH+xeQPz+Qr9mrjglfkYgtbVRA==} + oxc-parser@0.142.0: + resolution: {integrity: sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-resolver@11.20.0: - resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==} + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -3860,6 +3869,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -4174,8 +4187,8 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + smol-toml@1.7.1: + resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -4470,8 +4483,8 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - unbash@4.0.1: - resolution: {integrity: sha512-1ajSo3813sDoVIHx4inJdUS4l5L2ic5cFiddemPiyjb/PZEoBAhFwHtbaEdRDFxbAKy7FCG7s5ww3/uCFawuIA==} + unbash@4.0.6: + resolution: {integrity: sha512-YGBMSVG/WrA2vgaZbXVvxrGgoMcWZecyyS4foGt8clbtY7s1nIKNmt7Z9LR74XE6FkYTSqDmKk2lIOhOjIvmrw==} engines: {node: '>=14'} undefsafe@2.0.5: @@ -4661,8 +4674,8 @@ packages: peerDependencies: vue: ^3.0 - vue-tsc@3.2.6: - resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==} + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -4823,6 +4836,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: '@asamuzakjp/css-color@5.1.11': @@ -4943,6 +4959,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -4953,11 +4975,21 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.7.0)(supports-color@8.1.1))': dependencies: eslint: 10.0.3(jiti@2.7.0)(supports-color@8.1.1) @@ -5212,6 +5244,13 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@noble/hashes@1.8.0': optional: true @@ -5229,133 +5268,133 @@ snapshots: '@one-ini/wasm@0.1.1': {} - '@oxc-parser/binding-android-arm-eabi@0.135.0': + '@oxc-parser/binding-android-arm-eabi@0.142.0': optional: true - '@oxc-parser/binding-android-arm64@0.135.0': + '@oxc-parser/binding-android-arm64@0.142.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.135.0': + '@oxc-parser/binding-darwin-arm64@0.142.0': optional: true - '@oxc-parser/binding-darwin-x64@0.135.0': + '@oxc-parser/binding-darwin-x64@0.142.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.135.0': + '@oxc-parser/binding-freebsd-x64@0.142.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.135.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.142.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.135.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.142.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.135.0': + '@oxc-parser/binding-linux-arm64-gnu@0.142.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.135.0': + '@oxc-parser/binding-linux-arm64-musl@0.142.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.135.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.142.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.135.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.142.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.135.0': + '@oxc-parser/binding-linux-riscv64-musl@0.142.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.135.0': + '@oxc-parser/binding-linux-s390x-gnu@0.142.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.135.0': + '@oxc-parser/binding-linux-x64-gnu@0.142.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.135.0': + '@oxc-parser/binding-linux-x64-musl@0.142.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.135.0': + '@oxc-parser/binding-openharmony-arm64@0.142.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.135.0': + '@oxc-parser/binding-wasm32-wasi@0.142.0': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.135.0': + '@oxc-parser/binding-win32-arm64-msvc@0.142.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.135.0': + '@oxc-parser/binding-win32-ia32-msvc@0.142.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.135.0': + '@oxc-parser/binding-win32-x64-msvc@0.142.0': optional: true '@oxc-project/types@0.133.0': {} - '@oxc-project/types@0.135.0': {} + '@oxc-project/types@0.142.0': {} - '@oxc-resolver/binding-android-arm-eabi@11.20.0': + '@oxc-resolver/binding-android-arm-eabi@11.24.2': optional: true - '@oxc-resolver/binding-android-arm64@11.20.0': + '@oxc-resolver/binding-android-arm64@11.24.2': optional: true - '@oxc-resolver/binding-darwin-arm64@11.20.0': + '@oxc-resolver/binding-darwin-arm64@11.24.2': optional: true - '@oxc-resolver/binding-darwin-x64@11.20.0': + '@oxc-resolver/binding-darwin-x64@11.24.2': optional: true - '@oxc-resolver/binding-freebsd-x64@11.20.0': + '@oxc-resolver/binding-freebsd-x64@11.24.2': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0': + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': optional: true - '@oxc-resolver/binding-linux-arm-musleabihf@11.20.0': + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.20.0': + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.20.0': + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': optional: true - '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': optional: true - '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.20.0': + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.20.0': + '@oxc-resolver/binding-linux-x64-musl@11.24.2': optional: true - '@oxc-resolver/binding-openharmony-arm64@11.20.0': + '@oxc-resolver/binding-openharmony-arm64@11.24.2': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.20.0': + '@oxc-resolver/binding-wasm32-wasi@11.24.2': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.20.0': + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': optional: true '@pinia/testing@1.0.3(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))': @@ -6145,7 +6184,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vue/language-core@3.2.6': + '@vue/language-core@3.3.9': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.38 @@ -6153,7 +6192,7 @@ snapshots: alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 - picomatch: 4.0.4 + picomatch: 4.0.5 '@vue/reactivity@3.5.34': dependencies: @@ -7183,9 +7222,9 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 file-entry-cache@8.0.0: dependencies: @@ -7608,21 +7647,21 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@6.17.1: + knip@6.31.0: dependencies: - fdir: 6.5.0(picomatch@4.0.4) + fdir: 6.5.0(picomatch@4.0.5) formatly: 0.3.0 get-tsconfig: 4.14.0 jiti: 2.7.0 - oxc-parser: 0.135.0 - oxc-resolver: 11.20.0 - picomatch: 4.0.4 - smol-toml: 1.6.1 + oxc-parser: 0.142.0 + oxc-resolver: 11.24.2 + picomatch: 4.0.5 + smol-toml: 1.7.1 strip-json-comments: 5.0.3 tinyglobby: 0.2.17 - unbash: 4.0.1 + unbash: 4.0.6 yaml: 2.9.0 - zod: 4.3.6 + zod: 4.4.3 lazy-ass@1.6.0: {} @@ -7936,52 +7975,52 @@ snapshots: outdent@0.8.0: {} - oxc-parser@0.135.0: + oxc-parser@0.142.0: dependencies: - '@oxc-project/types': 0.135.0 + '@oxc-project/types': 0.142.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.135.0 - '@oxc-parser/binding-android-arm64': 0.135.0 - '@oxc-parser/binding-darwin-arm64': 0.135.0 - '@oxc-parser/binding-darwin-x64': 0.135.0 - '@oxc-parser/binding-freebsd-x64': 0.135.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.135.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.135.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.135.0 - '@oxc-parser/binding-linux-arm64-musl': 0.135.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.135.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.135.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.135.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.135.0 - '@oxc-parser/binding-linux-x64-gnu': 0.135.0 - '@oxc-parser/binding-linux-x64-musl': 0.135.0 - '@oxc-parser/binding-openharmony-arm64': 0.135.0 - '@oxc-parser/binding-wasm32-wasi': 0.135.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.135.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.135.0 - '@oxc-parser/binding-win32-x64-msvc': 0.135.0 - - oxc-resolver@11.20.0: + '@oxc-parser/binding-android-arm-eabi': 0.142.0 + '@oxc-parser/binding-android-arm64': 0.142.0 + '@oxc-parser/binding-darwin-arm64': 0.142.0 + '@oxc-parser/binding-darwin-x64': 0.142.0 + '@oxc-parser/binding-freebsd-x64': 0.142.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.142.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.142.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.142.0 + '@oxc-parser/binding-linux-arm64-musl': 0.142.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.142.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.142.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.142.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.142.0 + '@oxc-parser/binding-linux-x64-gnu': 0.142.0 + '@oxc-parser/binding-linux-x64-musl': 0.142.0 + '@oxc-parser/binding-openharmony-arm64': 0.142.0 + '@oxc-parser/binding-wasm32-wasi': 0.142.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.142.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.142.0 + '@oxc-parser/binding-win32-x64-msvc': 0.142.0 + + oxc-resolver@11.24.2: optionalDependencies: - '@oxc-resolver/binding-android-arm-eabi': 11.20.0 - '@oxc-resolver/binding-android-arm64': 11.20.0 - '@oxc-resolver/binding-darwin-arm64': 11.20.0 - '@oxc-resolver/binding-darwin-x64': 11.20.0 - '@oxc-resolver/binding-freebsd-x64': 11.20.0 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.20.0 - '@oxc-resolver/binding-linux-arm-musleabihf': 11.20.0 - '@oxc-resolver/binding-linux-arm64-gnu': 11.20.0 - '@oxc-resolver/binding-linux-arm64-musl': 11.20.0 - '@oxc-resolver/binding-linux-ppc64-gnu': 11.20.0 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.20.0 - '@oxc-resolver/binding-linux-riscv64-musl': 11.20.0 - '@oxc-resolver/binding-linux-s390x-gnu': 11.20.0 - '@oxc-resolver/binding-linux-x64-gnu': 11.20.0 - '@oxc-resolver/binding-linux-x64-musl': 11.20.0 - '@oxc-resolver/binding-openharmony-arm64': 11.20.0 - '@oxc-resolver/binding-wasm32-wasi': 11.20.0 - '@oxc-resolver/binding-win32-arm64-msvc': 11.20.0 - '@oxc-resolver/binding-win32-x64-msvc': 11.20.0 + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 p-limit@3.1.0: dependencies: @@ -8038,6 +8077,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pify@2.3.0: {} pinia-plugin-persistedstate-2@2.0.32(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))): @@ -8446,7 +8487,7 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - smol-toml@1.6.1: {} + smol-toml@1.7.1: {} source-map-js@1.2.1: {} @@ -8648,8 +8689,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} @@ -8761,7 +8802,7 @@ snapshots: uint8array-extras@1.5.0: {} - unbash@4.0.1: {} + unbash@4.0.6: {} undefsafe@2.0.5: {} @@ -8778,12 +8819,12 @@ snapshots: unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 - picomatch: 4.0.4 + picomatch: 4.0.5 unplugin@3.0.0: dependencies: '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.4 + picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 untildify@4.0.0: {} @@ -8815,7 +8856,7 @@ snapshots: vite@8.0.16(@types/node@22.19.15)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 + picomatch: 4.0.5 postcss: 8.5.16 rolldown: 1.0.3 tinyglobby: 0.2.17 @@ -8908,10 +8949,10 @@ snapshots: dependencies: vue: 3.5.34(typescript@6.0.3) - vue-tsc@3.2.6(typescript@6.0.3): + vue-tsc@3.3.9(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.2.6 + '@vue/language-core': 3.3.9 typescript: 6.0.3 vue3-spinners@1.3.3(vue@3.5.34(typescript@6.0.3)): @@ -9052,3 +9093,5 @@ snapshots: zod@3.25.76: {} zod@4.3.6: {} + + zod@4.4.3: {} diff --git a/server/package.json b/server/package.json index 2a6296c56..46364565e 100644 --- a/server/package.json +++ b/server/package.json @@ -3,11 +3,15 @@ "private": true, "type": "module", "main": "dist/index.js", - "exports": "./dist/index.js", + "exports": { + "@disco/source": "./src/index.ts", + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, "scripts": { "watch": "nodemon --ext ts --ignore dist --watch ../discojs-node/dist --watch . --exec pnpm run", "start": "node dist/main.js", - "build": "tsc --build", + "build": "tsc --build tsconfig.lib.json", "test": "cd .. && vitest --run --project=server" }, "author": "", diff --git a/server/tsconfig.lib.json b/server/tsconfig.lib.json index 82a87ade4..06455f74b 100644 --- a/server/tsconfig.lib.json +++ b/server/tsconfig.lib.json @@ -1,12 +1,16 @@ { - "extends": "../tsconfig.base.json", - "compilerOptions": { "rootDir": "./src", "outDir": "dist" }, + "extends": "../tsconfig.base.lib.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "dist", + "composite": true + }, "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.lib.json" }, { - "path": "../discojs-node" + "path": "../discojs-node/tsconfig.lib.json" } ], "include": ["src"] diff --git a/server/tsconfig.vitest.json b/server/tsconfig.vitest.json index b9d58ec27..135cacb84 100644 --- a/server/tsconfig.vitest.json +++ b/server/tsconfig.vitest.json @@ -1,13 +1,5 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { "noEmit": true }, - "references": [ - { - "path": "../discojs" - }, - { - "path": "../discojs-node" - } - ], "include": ["src", "tests"] } diff --git a/tsconfig.base.json b/tsconfig.base.json index a43c63956..8b5ab3610 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,6 +5,7 @@ "declarationMap": true, "module": "node16", + "moduleResolution": "node16", "target": "es2022", "strict": true, diff --git a/tsconfig.base.lib.json b/tsconfig.base.lib.json new file mode 100644 index 000000000..e2c16c267 --- /dev/null +++ b/tsconfig.base.lib.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "customConditions": ["@disco/source"] + } +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 486c9dc60..1dc1c369a 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.base.json", + "extends": "./tsconfig.base.lib.json", "include": ["**/src", "**/tests", "**/*config.ts"] } diff --git a/webapp/cypress/tsconfig.json b/webapp/cypress/tsconfig.json index c9e22991c..1640a71b8 100644 --- a/webapp/cypress/tsconfig.json +++ b/webapp/cypress/tsconfig.json @@ -3,10 +3,10 @@ "include": ["./e2e/**/*", "./support/**/*"], "references": [ { - "path": "../../discojs" + "path": "../../discojs/tsconfig.lib.json" }, { - "path": "../../discojs-web" + "path": "../../discojs-web/tsconfig.lib.json" } ], "compilerOptions": { diff --git a/webapp/package.json b/webapp/package.json index f62bacee9..e54a9e5e6 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "start": "vite", + "prebuild": "pnpm -F webapp^... run build", "build": "vue-tsc --build && vite build", "test": "pnpm run test:unit && pnpm run test:e2e", "test:unit": "vitest --run", @@ -49,7 +50,7 @@ "typescript": "catalog:", "vite": "8.0.16", "vitest": "catalog:", - "vue-tsc": "3.2.6", + "vue-tsc": "3.3.9", "vue3-spinners": "1.3.3" } } diff --git a/webapp/tsconfig.app.json b/webapp/tsconfig.app.json index 9de5abf33..6b13f7305 100644 --- a/webapp/tsconfig.app.json +++ b/webapp/tsconfig.app.json @@ -4,16 +4,15 @@ "exclude": ["src/**/__tests__/*"], "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.lib.json" }, { - "path": "../discojs-web" + "path": "../discojs-web/tsconfig.lib.json" } ], "compilerOptions": { "composite": true, "tsBuildInfoFile": "tsconfig.app.tsbuildinfo", - "paths": { "@/*": ["./src/*"] } diff --git a/webapp/tsconfig.node.json b/webapp/tsconfig.node.json index 713f9d902..dfa46af11 100644 --- a/webapp/tsconfig.node.json +++ b/webapp/tsconfig.node.json @@ -7,19 +7,10 @@ "nightwatch.conf.*", "playwright.config.*" ], - "references": [ - { - "path": "../discojs" - }, - { - "path": "../discojs-web" - } - ], "compilerOptions": { "composite": true, "noEmit": true, "tsBuildInfoFile": "tsconfig.node.tsbuildinfo", - "module": "ESNext", "moduleResolution": "Bundler", "types": ["node"] diff --git a/webapp/tsconfig.vitest.json b/webapp/tsconfig.vitest.json index 9ebc0647e..72650e0c8 100644 --- a/webapp/tsconfig.vitest.json +++ b/webapp/tsconfig.vitest.json @@ -1,18 +1,18 @@ { "extends": "./tsconfig.app.json", + // "include": ["src/**/__tests__/*", "env.d.ts"], "exclude": [], "references": [ { - "path": "../discojs" + "path": "../discojs/tsconfig.lib.json" }, { - "path": "../discojs-web" + "path": "../discojs-web/tsconfig.lib.json" } ], "compilerOptions": { "composite": true, "tsBuildInfoFile": "tsconfig.vitest.tsbuildinfo", - "lib": [], "types": ["node", "jsdom"] } From 5828701b1a3299ca6fc2b577ea9244012cc7e5cb Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 5 Aug 2026 18:30:40 +0200 Subject: [PATCH 4/5] refactor: use card.getModel instead of getModel --- cli/src/args.ts | 1 - discojs/src/default_tasks/cifar10.ts | 25 -------- discojs/src/default_tasks/lus_covid.ts | 62 ------------------- discojs/src/default_tasks/mnist.ts | 33 ---------- discojs/src/default_tasks/simple_face.ts | 14 ----- discojs/src/default_tasks/tinder_dog.ts | 56 ----------------- discojs/src/default_tasks/titanic.ts | 24 ------- discojs/src/default_tasks/wikitext.ts | 8 --- discojs/src/task/task_provider.ts | 3 +- docs/TASK.md | 32 +++++++--- docs/examples/custom_task.ts | 23 ------- server/src/task_set.ts | 2 +- server/tests/e2e/federated.spec.ts | 1 - server/tests/validator.spec.ts | 6 +- webapp/cypress/support/e2e.ts | 2 +- .../__tests__/TrainerDashboard.spec.ts | 2 +- 16 files changed, 30 insertions(+), 264 deletions(-) diff --git a/cli/src/args.ts b/cli/src/args.ts index 535cd34d7..13dcc806e 100644 --- a/cli/src/args.ts +++ b/cli/src/args.ts @@ -279,7 +279,6 @@ export const args: BenchmarkArguments = { return task; }, - getModel: () => provider.getModel(), modelCard: provider.modelCard, }, }; diff --git a/discojs/src/default_tasks/cifar10.ts b/discojs/src/default_tasks/cifar10.ts index c5f95c1dc..4cb375199 100644 --- a/discojs/src/default_tasks/cifar10.ts +++ b/discojs/src/default_tasks/cifar10.ts @@ -63,30 +63,5 @@ export const cifar10: TaskProvider<"image", "decentralized"> = { }); }, - async getModel(): Promise> { - const mobilenet = await tf.loadLayersModel({ - load: async () => Promise.resolve(baseModel), - }); - - const x = mobilenet.getLayer("global_average_pooling2d_1"); - const predictions = tf.layers - .dense({ units: 10, activation: "softmax", name: "denseModified" }) - .apply(x.output) as tf.SymbolicTensor; - - const model = tf.model({ - inputs: mobilenet.input, - outputs: predictions, - name: "modelModified", - }); - - model.compile({ - optimizer: "sgd", - loss: "categoricalCrossentropy", - metrics: ["accuracy"], - }); - - return new TFJS("image", model); - }, - modelCard: cards.CIFAR10Classifier, }; diff --git a/discojs/src/default_tasks/lus_covid.ts b/discojs/src/default_tasks/lus_covid.ts index c55f7d20b..32cdd08e5 100644 --- a/discojs/src/default_tasks/lus_covid.ts +++ b/discojs/src/default_tasks/lus_covid.ts @@ -44,67 +44,5 @@ export const lusCovid: TaskProvider<"image", "federated"> = { // Model architecture from tensorflow.js docs: // https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/index.html#4 - async getModel(): Promise> { - const imageHeight = 100; - const imageWidth = 100; - const imageChannels = 3; - const numOutputClasses = 2; - const model = tf.sequential(); - - // In the first layer of our convolutional neural network we have - // to specify the input shape. Then we specify some parameters for - // the convolution operation that takes place in this layer. - model.add( - tf.layers.conv2d({ - inputShape: [imageHeight, imageWidth, imageChannels], - kernelSize: 5, - filters: 8, - strides: 1, - activation: "relu", - kernelInitializer: "varianceScaling", - }), - ); - - // The MaxPooling layer acts as a sort of downsampling using max values - // in a region instead of averaging. - model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] })); - - // Repeat the conv2d + maxPooling block. - // Note that we have more filters in the convolution. - model.add( - tf.layers.conv2d({ - kernelSize: 5, - filters: 16, - strides: 1, - activation: "relu", - kernelInitializer: "varianceScaling", - }), - ); - model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] })); - - // Now we flatten the output from the 2D filters into a 1D vector to prepare - // it for input into our last layer. This is common practice when feeding - // higher dimensional data to a final classification output layer. - model.add(tf.layers.flatten()); - - // Our last layer is a dense layer which has 2 output units, one for each - // output class. - model.add( - tf.layers.dense({ - units: numOutputClasses, - kernelInitializer: "varianceScaling", - activation: "softmax", - }), - ); - - model.compile({ - optimizer: "sgd", - loss: "binaryCrossentropy", - metrics: ["accuracy"], - }); - - return Promise.resolve(new TFJS("image", model)); - }, - modelCard: cards.LUSClassifier, }; diff --git a/discojs/src/default_tasks/mnist.ts b/discojs/src/default_tasks/mnist.ts index b9bb94c00..c0346ba3e 100644 --- a/discojs/src/default_tasks/mnist.ts +++ b/discojs/src/default_tasks/mnist.ts @@ -50,38 +50,5 @@ export const mnist: TaskProvider<"image", "decentralized"> = { }); }, - getModel(): Promise> { - // Architecture from the PyTorch MNIST example (I made it slightly smaller, 650kB instead of 5MB) - // https://github.com/pytorch/examples/blob/main/mnist/main.py - const model = tf.sequential(); - - model.add( - tf.layers.conv2d({ - inputShape: [28, 28, 3], - kernelSize: 5, - filters: 8, - activation: "relu", - }), - ); - model.add( - tf.layers.conv2d({ kernelSize: 5, filters: 16, activation: "relu" }), - ); - model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 })); - model.add(tf.layers.dropout({ rate: 0.25 })); - - model.add(tf.layers.flatten()); - model.add(tf.layers.dense({ units: 32, activation: "relu" })); - model.add(tf.layers.dropout({ rate: 0.25 })); - model.add(tf.layers.dense({ units: 10, activation: "softmax" })); - - model.compile({ - optimizer: "adam", - loss: "categoricalCrossentropy", - metrics: ["accuracy"], - }); - - return Promise.resolve(new TFJS("image", model)); - }, - modelCard: cards.PytorchMNISTClassifier, }; diff --git a/discojs/src/default_tasks/simple_face.ts b/discojs/src/default_tasks/simple_face.ts index 120b3bfc9..e403607a9 100644 --- a/discojs/src/default_tasks/simple_face.ts +++ b/discojs/src/default_tasks/simple_face.ts @@ -39,19 +39,5 @@ export const simpleFace: TaskProvider<"image", "federated"> = { }); }, - async getModel(): Promise> { - const model = await tf.loadLayersModel({ - load: async () => Promise.resolve(baseModel), - }); - - model.compile({ - optimizer: tf.train.sgd(0.001), - loss: "categoricalCrossentropy", - metrics: ["accuracy"], - }); - - return new TFJS("image", model); - }, - modelCard: cards.FaceClassifier, }; diff --git a/discojs/src/default_tasks/tinder_dog.ts b/discojs/src/default_tasks/tinder_dog.ts index c728c6f3b..400b9fe2c 100644 --- a/discojs/src/default_tasks/tinder_dog.ts +++ b/discojs/src/default_tasks/tinder_dog.ts @@ -40,61 +40,5 @@ export const tinderDog: TaskProvider<"image", "federated"> = { }); }, - async getModel(): Promise> { - const task = await this.getTask(); - - const seed = 42; // set a seed to ensure reproducibility during GDHF demo - const imageHeight = task.trainingInformation.IMAGE_H; - const imageWidth = task.trainingInformation.IMAGE_W; - const imageChannels = 3; - - const model = tf.sequential(); - - model.add( - tf.layers.conv2d({ - inputShape: [imageHeight, imageWidth, imageChannels], - kernelSize: 5, - filters: 8, - activation: "relu", - kernelInitializer: tf.initializers.heNormal({ seed }), - }), - ); - model.add( - tf.layers.conv2d({ - kernelSize: 5, - filters: 16, - activation: "relu", - kernelInitializer: tf.initializers.heNormal({ seed }), - }), - ); - model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 })); - model.add(tf.layers.dropout({ rate: 0.25, seed })); - - model.add(tf.layers.flatten()); - model.add( - tf.layers.dense({ - units: 32, - activation: "relu", - kernelInitializer: tf.initializers.heNormal({ seed }), - }), - ); - model.add(tf.layers.dropout({ rate: 0.25, seed })); - model.add( - tf.layers.dense({ - units: 2, - activation: "softmax", - kernelInitializer: tf.initializers.heNormal({ seed }), - }), - ); - - model.compile({ - optimizer: tf.train.adam(0.0005), - loss: "categoricalCrossentropy", - metrics: ["accuracy"], - }); - - return Promise.resolve(new TFJS("image", model)); - }, - modelCard: cards.DogClassifier, }; diff --git a/discojs/src/default_tasks/titanic.ts b/discojs/src/default_tasks/titanic.ts index a038d2abe..3e74c28b7 100644 --- a/discojs/src/default_tasks/titanic.ts +++ b/discojs/src/default_tasks/titanic.ts @@ -53,29 +53,5 @@ export const titanic: TaskProvider<"tabular", "federated"> = { }); }, - getModel(): Promise> { - const model = tf.sequential(); - - model.add( - tf.layers.dense({ - inputShape: [5], - units: 124, - activation: "relu", - kernelInitializer: "leCunNormal", - }), - ); - model.add(tf.layers.dense({ units: 64, activation: "relu" })); - model.add(tf.layers.dense({ units: 32, activation: "relu" })); - model.add(tf.layers.dense({ units: 1, activation: "sigmoid" })); - - model.compile({ - optimizer: "adam", - loss: "binaryCrossentropy", - metrics: ["accuracy"], - }); - - return Promise.resolve(new TFJS("tabular", model)); - }, - modelCard: cards.TitanicClassifier, }; diff --git a/discojs/src/default_tasks/wikitext.ts b/discojs/src/default_tasks/wikitext.ts index 2f73b645c..ee87d0f8b 100644 --- a/discojs/src/default_tasks/wikitext.ts +++ b/discojs/src/default_tasks/wikitext.ts @@ -48,13 +48,5 @@ export const wikitext: TaskProvider<"text", "federated"> = { }; }, - async getModel() { - const task = await this.getTask(); - - return new GPT({ - contextLength: task.trainingInformation.contextLength, - }); - }, - modelCard: cards.Wikitext, }; diff --git a/discojs/src/task/task_provider.ts b/discojs/src/task/task_provider.ts index 5f6a150ae..666856f85 100644 --- a/discojs/src/task/task_provider.ts +++ b/discojs/src/task/task_provider.ts @@ -1,8 +1,7 @@ -import type { DataType, Model, Network, Task, ModelCard } from "../index.js"; +import type { DataType, Network, Task, ModelCard } from "../index.js"; export interface TaskProvider { getTask(): Promise>; // Create the corresponding model ready for training (compiled) - getModel(): Promise>; modelCard: ModelCard; } diff --git a/docs/TASK.md b/docs/TASK.md index 8f214a5ef..3c61ea9c0 100644 --- a/docs/TASK.md +++ b/docs/TASK.md @@ -51,10 +51,10 @@ The [task creation form](https://discolab.ai/#/create) lets users create a custo ## 3. Implementing custom tasks Programming skills are necessary to add a custom task not supported by the task creation UI. -A task is mainly defined by a `TaskProvider` which needs to implement two methods: +A task is mainly defined by a `TaskProvider` which needs to implement two things: - `getTask` which returns a `Task` as defined by the [Task interface](../discojs/src/task/task.ts). The `Task` contains all the crucial information from training to the mode -- `getModel` which returns a `Promise` specifying a model architecture for the task +- `modelCard` which itself implements `getModel`, returning a `Promise` specifying a model architecture for the task. It also has information about the model. You can add a new task in two different ways: @@ -76,11 +76,18 @@ const customTask: TaskProvider = { }; }, - async getModel(): Promise { - const model = tf.sequential(); - // Configure your model architecture - return model; - }, + modelCard: { + { + card: { + // information here + }; + getModel(): : Promise { + const model = tf.sequential(); + // Configure your model architecture + return model; + }, + } + } }; async function runServer() { @@ -177,8 +184,15 @@ export const customTask: TaskProvider = { } }, - async getModel (): Promise { - throw new Error('Not implemented') + modelCard: { + { + card: { + // information here + }; + getModel(): : Promise { + throw new Error('Not implemented') + }, + } } } ``` diff --git a/docs/examples/custom_task.ts b/docs/examples/custom_task.ts index 0c9aa1f08..771236183 100644 --- a/docs/examples/custom_task.ts +++ b/docs/examples/custom_task.ts @@ -65,29 +65,6 @@ const customTask: TaskProvider<"tabular", "federated"> = { }); }, - getModel() { - const model = tf.sequential(); - - model.add( - tf.layers.dense({ - inputShape: [1], - units: 124, - activation: "relu", - kernelInitializer: "leCunNormal", - }), - ); - model.add(tf.layers.dense({ units: 32, activation: "relu" })); - model.add(tf.layers.dense({ units: 1, activation: "sigmoid" })); - - model.compile({ - optimizer: "rmsprop", - loss: "binaryCrossentropy", - metrics: ["accuracy"], - }); - - return Promise.resolve(new models.TFJS("tabular", model)); - }, - modelCard: customModelCard, }; diff --git a/server/src/task_set.ts b/server/src/task_set.ts index 78b0b6d38..6a786a877 100644 --- a/server/src/task_set.ts +++ b/server/src/task_set.ts @@ -124,7 +124,7 @@ export class TaskSet extends EventEmitter<{ // to get the model architecture definition throw new Error("saved model not found and no way to get it"); } - const model = await taskOrProvider.getModel(); + const model = await taskOrProvider.modelCard.getModel(); // Save the model to disk await fs.mkdir(modelPath, { recursive: true }); diff --git a/server/tests/e2e/federated.spec.ts b/server/tests/e2e/federated.spec.ts index dab15f9d3..21869a350 100644 --- a/server/tests/e2e/federated.spec.ts +++ b/server/tests/e2e/federated.spec.ts @@ -83,7 +83,6 @@ describe("end-to-end federated", () => { }, }; const url = await startServer({ - getModel: () => defaultTasks.cifar10.getModel(), getTask: () => Promise.resolve(cifar10Task), modelCard: defaultModels.CIFAR10Classifier, }); diff --git a/server/tests/validator.spec.ts b/server/tests/validator.spec.ts index 8f3ebebc3..3b3d8d433 100644 --- a/server/tests/validator.spec.ts +++ b/server/tests/validator.spec.ts @@ -12,7 +12,7 @@ describe("validator", () => { const validator = new Validator( await provider.getTask(), - await provider.getModel(), + await provider.modelCard.getModel(), ); let hits = 0; @@ -35,7 +35,7 @@ describe("validator", () => { const validator = new Validator( await provider.getTask(), - await provider.getModel(), + await provider.modelCard.getModel(), ); let hits = 0; @@ -63,7 +63,7 @@ describe("validator", () => { const validator = new Validator( task, - await defaultTasks.lusCovid.getModel(), + await defaultTasks.lusCovid.modelCard.getModel(), ); let hits = 0; diff --git a/webapp/cypress/support/e2e.ts b/webapp/cypress/support/e2e.ts index 34951d125..119b229bc 100644 --- a/webapp/cypress/support/e2e.ts +++ b/webapp/cypress/support/e2e.ts @@ -16,7 +16,7 @@ export function setupServerWith( Promise.all( providers.map(async (p) => { if ("id" in p) return [p, undefined] as const; - return [await p.getTask(), await p.getModel()] as const; + return [await p.getTask(), await p.modelCard.getModel()] as const; }), ), ) diff --git a/webapp/src/components/training/__tests__/TrainerDashboard.spec.ts b/webapp/src/components/training/__tests__/TrainerDashboard.spec.ts index 6984051b1..2374369e2 100644 --- a/webapp/src/components/training/__tests__/TrainerDashboard.spec.ts +++ b/webapp/src/components/training/__tests__/TrainerDashboard.spec.ts @@ -17,7 +17,7 @@ async function setupForTask() { url.toString() === new URL("tasks/titanic/model.json", CONFIG.serverUrl).href ) { - const model = await provider.getModel(); + const model = await provider.modelCard.getModel(); const encoded = await serialization.model.encode(model); return new Response(new Uint8Array(encoded)); } From 8e008d6e128a5b8789326b760ac6b741e4eade5f Mon Sep 17 00:00:00 2001 From: hugo Date: Fri, 7 Aug 2026 11:30:06 +0200 Subject: [PATCH 5/5] refactor: split tasks and models in server --- cli/src/benchmark_gpt.ts | 6 +- discojs/src/index.ts | 1 + discojs/src/models/cards/LUSClassifier.ts | 1 + discojs/src/models/cards/dogClassifier.ts | 1 + discojs/src/models/cards/faceClassifier.ts | 1 + .../src/models/cards/mobileNetV1_cifar10.ts | 1 + .../models/cards/pytorchMnistClassifier.ts | 1 + discojs/src/models/cards/titanicClassifier.ts | 1 + discojs/src/models/cards/wikitext.ts | 1 + discojs/src/models/index.ts | 2 +- discojs/src/models/model_card.ts | 27 +++- discojs/src/types/index.ts | 3 +- docs/examples/custom_task.ts | 8 +- docs/examples/training.ts | 6 +- server/src/main.ts | 17 ++- server/src/model_set.ts | 93 ++++++++++++++ server/src/routes/model_router.ts | 29 +++++ server/src/routes/task_router.ts | 9 +- server/src/server.ts | 29 ++++- server/src/task_set.ts | 120 ++++-------------- server/tests/client.spec.ts | 32 +++-- server/tests/e2e/decentralized.spec.ts | 37 ++++-- server/tests/e2e/federated.spec.ts | 41 ++++-- 23 files changed, 314 insertions(+), 153 deletions(-) create mode 100644 server/src/model_set.ts create mode 100644 server/src/routes/model_router.ts diff --git a/cli/src/benchmark_gpt.ts b/cli/src/benchmark_gpt.ts index 36fec2507..26fb71c75 100644 --- a/cli/src/benchmark_gpt.ts +++ b/cli/src/benchmark_gpt.ts @@ -6,6 +6,7 @@ import type { Network, Task } from "@epfml/discojs"; import { async_iterator, defaultTasks, + defaultModels, fetchTasks, models, } from "@epfml/discojs"; @@ -86,7 +87,10 @@ async function main(args: Required): Promise { } = args; // Launch a server instance - const server = await Server.with(defaultTasks.wikitext); + const server = await Server.with( + [defaultModels.Wikitext], + [defaultTasks.wikitext], + ); const [handle, url] = await server.serve(); // Fetch the wikitext task from the server diff --git a/discojs/src/index.ts b/discojs/src/index.ts index 4ae4782ce..375fa6193 100644 --- a/discojs/src/index.ts +++ b/discojs/src/index.ts @@ -19,6 +19,7 @@ export { Validator } from "./validator.js"; export { Model, ModelCard, + ModelCardInfo, BatchLogs, EpochLogs, Tokenizer, diff --git a/discojs/src/models/cards/LUSClassifier.ts b/discojs/src/models/cards/LUSClassifier.ts index ba107350e..cc85d0f06 100644 --- a/discojs/src/models/cards/LUSClassifier.ts +++ b/discojs/src/models/cards/LUSClassifier.ts @@ -6,6 +6,7 @@ export const LUSClassifier: ModelCard<"image"> = { card: { id: "lus_classifier", name: "LUS Classifier", + dataType: "image", preTrained: false, }, diff --git a/discojs/src/models/cards/dogClassifier.ts b/discojs/src/models/cards/dogClassifier.ts index 9cbf17bda..e015b64af 100644 --- a/discojs/src/models/cards/dogClassifier.ts +++ b/discojs/src/models/cards/dogClassifier.ts @@ -5,6 +5,7 @@ export const DogClassifier: ModelCard<"image"> = { card: { id: "dog_classifier", name: "Dog Classifier", + dataType: "image", preTrained: false, }, diff --git a/discojs/src/models/cards/faceClassifier.ts b/discojs/src/models/cards/faceClassifier.ts index b8655d8cf..0209b8aca 100644 --- a/discojs/src/models/cards/faceClassifier.ts +++ b/discojs/src/models/cards/faceClassifier.ts @@ -5,6 +5,7 @@ export const FaceClassifier: ModelCard<"image"> = { card: { id: "face_classifier", name: "Face Classifier", + dataType: "image", preTrained: false, }, diff --git a/discojs/src/models/cards/mobileNetV1_cifar10.ts b/discojs/src/models/cards/mobileNetV1_cifar10.ts index cb2980a2d..20b015677 100644 --- a/discojs/src/models/cards/mobileNetV1_cifar10.ts +++ b/discojs/src/models/cards/mobileNetV1_cifar10.ts @@ -5,6 +5,7 @@ export const CIFAR10Classifier: ModelCard<"image"> = { card: { id: "mobile_net_v1_cifar10", name: "Pre-trained CIFAR-10 Classifier", + dataType: "image", preTrained: true, }, diff --git a/discojs/src/models/cards/pytorchMnistClassifier.ts b/discojs/src/models/cards/pytorchMnistClassifier.ts index dfd10a6f0..58884c274 100644 --- a/discojs/src/models/cards/pytorchMnistClassifier.ts +++ b/discojs/src/models/cards/pytorchMnistClassifier.ts @@ -5,6 +5,7 @@ export const PytorchMNISTClassifier: ModelCard<"image"> = { card: { id: "mnist_classifier", name: "MNIST Classifier", + dataType: "image", preTrained: false, }, diff --git a/discojs/src/models/cards/titanicClassifier.ts b/discojs/src/models/cards/titanicClassifier.ts index 053d8f8bb..db2001e53 100644 --- a/discojs/src/models/cards/titanicClassifier.ts +++ b/discojs/src/models/cards/titanicClassifier.ts @@ -5,6 +5,7 @@ export const TitanicClassifier: ModelCard<"tabular"> = { card: { id: "titanic_classifier", name: "Titanic classifier", + dataType: "tabular", preTrained: false, }, diff --git a/discojs/src/models/cards/wikitext.ts b/discojs/src/models/cards/wikitext.ts index b617507f3..2747e5f37 100644 --- a/discojs/src/models/cards/wikitext.ts +++ b/discojs/src/models/cards/wikitext.ts @@ -5,6 +5,7 @@ export const Wikitext: ModelCard<"text"> = { card: { id: "wikitext", name: "Wikitext GPT-2", + dataType: "text", preTrained: false, contextLength: 64, }, diff --git a/discojs/src/models/index.ts b/discojs/src/models/index.ts index 96406e2dc..f0c6a67d0 100644 --- a/discojs/src/models/index.ts +++ b/discojs/src/models/index.ts @@ -15,6 +15,6 @@ export { HELLASWAG_URL, } from "./implementations/hellaswag.js"; export { TFJS } from "./tfjs.js"; -export type { ModelCard } from "./model_card.js"; +export type { ModelCard, ModelCardInfo } from "./model_card.js"; export * as cards from "./cards/index.js"; diff --git a/discojs/src/models/model_card.ts b/discojs/src/models/model_card.ts index 45f913528..f021ead6e 100644 --- a/discojs/src/models/model_card.ts +++ b/discojs/src/models/model_card.ts @@ -1,21 +1,38 @@ import { z } from "zod"; import type { Model } from "#models/index"; -import type { DataType } from "#dtypes/index"; +import { DataType, dataTypeValues } from "#dtypes/index"; -namespace ModelCardInfo { +export namespace ModelCardInfo { export type ID = string; - export const schema = z.object({ + export const baseFields = { id: z.string(), name: z.string(), preTrained: z.boolean(), contextLength: z.number().optional(), + }; + + export const schema = z.object({ + ...baseFields, + dataType: z.enum(dataTypeValues), }); + + // Runtime-checked schema for each data type + export function dataTypedSchema(dataType: D) { + return z.object({ + ...baseFields, + dataType: z.literal(dataType), + }); + } + + export type Of = z.infer< + ReturnType> + >; } -type ModelCardInfo = z.infer; +export type ModelCardInfo = ModelCardInfo.Of; export interface ModelCard { - card: ModelCardInfo; + card: ModelCardInfo; getModel(): Promise>; } diff --git a/discojs/src/types/index.ts b/discojs/src/types/index.ts index 8c6916095..a8f8ef7fa 100644 --- a/discojs/src/types/index.ts +++ b/discojs/src/types/index.ts @@ -1,4 +1,5 @@ export * as DataFormat from "./data_format.js"; -export type DataType = "image" | "tabular" | "text"; +export const dataTypeValues = ["image", "tabular", "text"] as const; +export type DataType = (typeof dataTypeValues)[number]; export type Network = "decentralized" | "federated" | "local"; diff --git a/docs/examples/custom_task.ts b/docs/examples/custom_task.ts index 771236183..77daccec1 100644 --- a/docs/examples/custom_task.ts +++ b/docs/examples/custom_task.ts @@ -1,7 +1,7 @@ import tf from "@tensorflow/tfjs-node"; import type { TaskProvider, ModelCard } from "@epfml/discojs"; -import { defaultTasks, models } from "@epfml/discojs"; +import { defaultTasks, defaultModels, models } from "@epfml/discojs"; import { Server as DiscoServer } from "server"; // Define your own model card @@ -9,6 +9,7 @@ const customModelCard: ModelCard<"tabular"> = { card: { id: "custom_model_id", name: "Custom name", + dataType: "tabular", preTrained: false, }, @@ -71,8 +72,9 @@ const customTask: TaskProvider<"tabular", "federated"> = { async function runServer(): Promise { // Create a server const server = await DiscoServer.with( - defaultTasks.titanic, // with some tasks provided by Disco - customTask, // or your own custom task + // with some tasks provided by Disco, or your own custom task + [defaultModels.TitanicClassifier, customModelCard], + [defaultTasks.titanic, customTask], ); // Start the server diff --git a/docs/examples/training.ts b/docs/examples/training.ts index 144f8dd8e..a14f926f8 100644 --- a/docs/examples/training.ts +++ b/docs/examples/training.ts @@ -9,7 +9,7 @@ import type { Image, Task, } from "@epfml/discojs"; -import { Disco, fetchTasks, defaultTasks } from "@epfml/discojs"; +import { Disco, fetchTasks, defaultTasks, defaultModels } from "@epfml/discojs"; import { loadCSV, loadImagesInDir } from "@epfml/discojs-node"; import { Server } from "server"; @@ -43,8 +43,8 @@ async function main(): Promise { // Launch a server instance const server = await Server.with( - defaultTasks.simpleFace, - defaultTasks.titanic, + [defaultModels.FaceClassifier, defaultModels.TitanicClassifier], + [(defaultTasks.simpleFace, defaultTasks.titanic)], ); const [handle, url] = await server.serve(); diff --git a/server/src/main.ts b/server/src/main.ts index f49996f8f..9d370c192 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -5,17 +5,18 @@ */ import type { DataType, Network, Task } from "@epfml/discojs"; -import { defaultTasks } from "@epfml/discojs"; +import { defaultTasks, defaultModels } from "@epfml/discojs"; import { Server } from "./server.js"; const PORT = 8080; -const providers = Object.values(defaultTasks); +const models = Object.values(defaultModels); +const taskProviders = Object.values(defaultTasks); console.info("Server loaded the tasks below"); console.table( - (await Promise.all(providers.map((p) => p.getTask()))).map( + (await Promise.all(taskProviders.map((p) => p.getTask()))).map( (task: Task) => ({ ID: task.id, Title: task.displayInformation.title, @@ -24,8 +25,16 @@ console.table( }), ), ); +console.table( + models.map((m) => ({ + ID: m.card.id, + Title: m.card.name, + "Data Type": m.card.dataType, + Pretrained: m.card.preTrained, + })), +); // Init the server with default tasks -const server = await Server.with(...providers); +const server = await Server.with(models, taskProviders); const [_, serverURL] = await server.serve(PORT); console.log(`Disco Server listening on ${serverURL.toString()}`); diff --git a/server/src/model_set.ts b/server/src/model_set.ts new file mode 100644 index 000000000..9121aaf54 --- /dev/null +++ b/server/src/model_set.ts @@ -0,0 +1,93 @@ +import { Map } from "immutable"; +import "@tensorflow/tfjs-node"; + +import type { DataType, ModelCardInfo, ModelCard } from "@epfml/discojs"; +import { EventEmitter, Model, serialization } from "@epfml/discojs"; + +type EncodedModel = serialization.Encoded; +type AvailableModel = [ModelCardInfo, EncodedModel]; + +/** + * The ModelSet stores available encoded models along with their information. + * Models can then be fed to Tasks. + * + * We work with EncodedModels rather than Models because they are sent encoded + * to clients. Since the server doesn't need to use the Model, we + * simply leave it already encoded and ready to be sent to clients + * + * Due to the asynchronous nature of `addModel`, ModelSet is an EventEmitter, + * by registering callbacks on new models and emitting a 'newModel' event + * when a new model has been added. + * + * Models are usually passed to ModelSet when booting the server + * and objects depending on models can subscribe to the 'newModel' + * event to run callbacks whenever a new EncodedModel is initialized. + */ +export class ModelSet extends EventEmitter<{ + newModel: AvailableModel; +}> { + // Keep track of previously initialized models + #models = Map(); + + get models(): Map { + return this.#models; + } + + // send known models to new listener + override on( + _: "newModel", + listener: (_: AvailableModel) => void | Promise, + ): void { + this.#models.forEach(listener); + } + + /** + * Method to add a new model. + * It accepts parameters in different formats, as long as it contains + * model info and some way to load a model. + * The method emits a 'newModel' event with the resulting info and EncodedModel. + * + * If information and EncodedModel are provided as parameters the method + * does not change them. + * Otherwise the method handles shaping the parameters into ModelCardInfo + * and EncodedModel before emitting the event. + * + * Finally, encoded models are stored on the disk the first time they are + * seen, so that they can be reused more easily + */ + async addModel( + newModel: + | [ModelCardInfo, Model] + | [ModelCardInfo, EncodedModel] + | ModelCard, + ): Promise { + // get info first to check if model already exists + let info: ModelCardInfo; + if (!Array.isArray(newModel)) { + info = newModel.card; + } else { + info = newModel[0]; + } + + if (this.#models.has(info.id)) { + throw new Error("model already exists"); + } + + let encodedModel: EncodedModel; + if (!Array.isArray(newModel)) { + const model = await newModel.getModel(); + encodedModel = await serialization.model.encode(model); + } else { + const model = newModel[1]; + if (serialization.isEncoded(model)) { + encodedModel = model; // don't do anything if already encoded + } else { + encodedModel = await serialization.model.encode(model); + } + } + + // Add the task-model pair to the set + this.#models = this.#models.set(info.id, [info, encodedModel]); + this.emit("newModel", [info, encodedModel]); + } +} diff --git a/server/src/routes/model_router.ts b/server/src/routes/model_router.ts new file mode 100644 index 000000000..6000600ae --- /dev/null +++ b/server/src/routes/model_router.ts @@ -0,0 +1,29 @@ +import express from "express"; + +import type { ModelSet } from "../model_set.js"; + +export class ModelRouter { + readonly #expressRouter: express.Router; + readonly #modelSet: ModelSet; + + constructor(modelSet: ModelSet) { + this.#modelSet = modelSet; + this.#expressRouter = express.Router(); + + // Return available models upon GET requests + this.#expressRouter.get("/", (_, res) => { + res.status(200).send( + this.#modelSet.models + .valueSeq() + .map(([modelInfo, _]) => modelInfo) + .toArray(), + ); + }); + + this.#expressRouter.use(express.json()); + } + + public get router(): express.Router { + return this.#expressRouter; + } +} diff --git a/server/src/routes/task_router.ts b/server/src/routes/task_router.ts index efa5d0515..88e7eb7b0 100644 --- a/server/src/routes/task_router.ts +++ b/server/src/routes/task_router.ts @@ -42,10 +42,7 @@ export class TaskRouter { this.#expressRouter.post("/", async (req, res) => { const parsed = await z .object({ - model: z - .array(z.number()) - .transform((arr) => Uint8Array.from(arr)) - .transform(serialization.model.decode), + model: z.string(), task: z.any().transform(serialization.task.deserializeFromJSON), }) .safeParseAsync(req.body); @@ -58,10 +55,10 @@ export class TaskRouter { const { model, task } = parsed.data; try { - await this.#taskSet.addTask(task, model); + this.#taskSet.addTask(task, model); } catch (e) { debug("add task failed with: %o", e); - if (e instanceof Error && e.message === "already existing") + if (e instanceof Error && e.message === "added task already exists") res.status(409).end(); else res.status(500).end(); return; diff --git a/server/src/server.ts b/server/src/server.ts index 7bbb1e917..5fdc92bb1 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -3,10 +3,17 @@ import express from "express"; import expressWS from "express-ws"; import type * as http from "http"; -import type { DataType, Network, TaskProvider } from "@epfml/discojs"; +import type { + DataType, + Network, + ModelCard, + TaskProvider, +} from "@epfml/discojs"; import { TaskRouter, TrainingRouter } from "./routes/index.js"; import { TaskSet } from "./task_set.js"; +import { ModelSet } from "./model_set.js"; +import { ModelRouter } from "./routes/model_router.js"; /** * The Disco Server, initializing an Express app @@ -17,15 +24,27 @@ import { TaskSet } from "./task_set.js"; * https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/Introduction */ export class Server { - readonly #taskSet = new TaskSet(); + readonly #modelSet; + readonly #taskSet; + + constructor() { + this.#modelSet = new ModelSet(); + this.#taskSet = new TaskSet(this.#modelSet); + } /** setup with given initial tasks */ static async with( - ...tasks: TaskProvider[] + models: ModelCard[], + tasks: TaskProvider[], ): Promise { const server = new Server(); - await Promise.all(tasks.map((t) => server.#taskSet.addTask(t))); + await Promise.all(models.map((m) => server.#modelSet.addModel(m))); + await Promise.all( + tasks.map(async (t) => + server.#taskSet.addTask(await t.getTask(), t.modelCard.card.id), + ), + ); return server; } @@ -49,6 +68,7 @@ export class Server { app.use(express.urlencoded({ limit: "50mb", extended: false })); const taskRouter = new TaskRouter(this.#taskSet); + const modelRouter = new ModelRouter(this.#modelSet); const federatedRouter = new TrainingRouter( "federated", wsApplier, @@ -67,6 +87,7 @@ export class Server { app.use("/federated", federatedRouter.router); app.use("/decentralized", decentralizedRouter.router); app.use("/tasks", taskRouter.router); + app.use("/models", modelRouter.router); const server = await new Promise((resolve, reject) => { const ret = app.listen(port); diff --git a/server/src/task_set.ts b/server/src/task_set.ts index 6a786a877..149fbea1a 100644 --- a/server/src/task_set.ts +++ b/server/src/task_set.ts @@ -1,9 +1,9 @@ import { Map } from "immutable"; -import fs from "node:fs/promises"; import "@tensorflow/tfjs-node"; -import type { DataType, Network, Task, TaskProvider } from "@epfml/discojs"; -import { EventEmitter, Model, serialization } from "@epfml/discojs"; +import type { DataType, Network, Task } from "@epfml/discojs"; +import { EventEmitter, serialization } from "@epfml/discojs"; +import { ModelSet } from "./model_set.js"; type EncodedModel = serialization.Encoded; type TaskAndModel = [Task, EncodedModel]; @@ -12,29 +12,25 @@ type TaskAndModel = [Task, EncodedModel]; * The TaskSet essentially handles initializing a Task and * loading its associated EncodedModel. * - * We rely on a TaskSet to abstract the (asynchronous) logic of getting the model - * when not provided. - * Depending on the case, getting the model is done by reading the model files - * from disk if they exists, downloading them from a URL or - * initializing the model from its architecture definition. - * - * We work with EncodedModels rather than Models because they are sent encoded - * to clients. Since the server doesn't need to use the Model, we - * simply leave it already encoded and ready to be sent to clients - * - * Due to the asynchronous nature of `addTask`, TaskSet is an EventEmitter, - * by registering callbacks on new tasks and emitting a 'newTask' event - * when a new task has been added. + * TaskSet is an EventEmitter, registering callbacks on new tasks and + * emitting a 'newTask' event when a new task has been added. * * Tasks are usually passed to TaskSet when booting the server * and objects depending on tasks and models can subscribe to - * the 'newTask' event to run callbacks whenever a new Task and EncodedModel are initialized. + * the 'newTask' event to run callbacks whenever a new Task and + * EncodedModel are initialized. */ export class TaskSet extends EventEmitter<{ newTask: TaskAndModel; }> { // Keep track of previously initialized task-model pairs #tasks = Map(); + #modelSet: ModelSet; + + constructor(modelSet: ModelSet) { + super(); + this.#modelSet = modelSet; + } get tasks(): Map { return this.#tasks; @@ -49,88 +45,24 @@ export class TaskSet extends EventEmitter<{ } /** - * Method to add a new task and optionally its associated model. - * It accepts parameters in different formats and handles - * shaping them into a Task and an EncodedModel. - * The method emits a 'newTask' event with the resulting Task and EncodedModel. - * - * If a Task and the EncodedModel is provided as parameters the method does change them - * Otherwise the method handles shaping the parameters into a Task and EncodedModel - * before emitting the event - * - * @param taskOrProvider either a Task or TaskProvider - * @param model optional model, can already be an EncodedModel, a Model or a URL for the model + * Method to add a new task. + * The method emits a 'newTask' event with the resulting Task. */ - async addTask( - taskOrProvider: Task | TaskProvider, - model?: Model | EncodedModel, - ): Promise { - // get the task - const task = - "getTask" in taskOrProvider - ? await taskOrProvider.getTask() - : taskOrProvider; - - // get the model - let encodedModel: EncodedModel; - if (serialization.isEncoded(model)) { - encodedModel = model; // don't do anything if already encoded - } else { - let tfModel: Model; - if (model === undefined) { - // Get the model if nothing is provided - tfModel = await this.loadModelFromTask(taskOrProvider); - } else if (model instanceof Model) { - // Don't do anything if the model is already specified - tfModel = model; - } else { - throw new Error("invalid model"); - } - encodedModel = await serialization.model.encode(tfModel); + addTask(task: Task, model_id: string): void { + if (this.#tasks.has(task.id)) { + throw new Error("added task already exists"); } - // Add the task-model pair to the set - if (this.#tasks.has(task.id)) throw new Error("already existing"); - this.#tasks = this.#tasks.set(task.id, [task, encodedModel]); - this.emit("newTask", [task, encodedModel]); - } - - /** - * Gets the model associated to a task. First checks if the model has been saved to disk. - * Otherwise, initializes it from its architecture definition (and saves it to disk) - * - * @param taskOrProvider either a Task or a TaskProvider - * @returns a promise for the associated model - */ - private async loadModelFromTask( - taskOrProvider: Task | TaskProvider, - ): Promise> { - const task = - "getTask" in taskOrProvider - ? await taskOrProvider.getTask() - : taskOrProvider; - - const modelPath = `./models/${task.id}/`; - try { - const content = await fs.readFile(`${modelPath}/model.json`); - // cast as we trust the task ID - return await serialization.model.decode(content); - } catch { - // unable to read file (potentially doesn't exist), continuing + const model = this.#modelSet.models.get(model_id); + if (model === undefined) { + throw new Error("referenced model unavailable"); } - - if ("id" in taskOrProvider) { - // if the model isn't already saved to disk then we need the TaskProvider - // to get the model architecture definition - throw new Error("saved model not found and no way to get it"); + if (model[0].dataType !== task.dataType) { + throw new Error("task and model data types do not match"); } - const model = await taskOrProvider.modelCard.getModel(); - - // Save the model to disk - await fs.mkdir(modelPath, { recursive: true }); - const encoded = await serialization.model.encode(model); - await fs.writeFile(`${modelPath}/model.json`, encoded); + const encodedModel = model[1]; - return model; + this.#tasks = this.#tasks.set(task.id, [task, encodedModel]); + this.emit("newTask", [task, encodedModel]); } } diff --git a/server/tests/client.spec.ts b/server/tests/client.spec.ts index 4a0b435c6..ccdfd40e3 100644 --- a/server/tests/client.spec.ts +++ b/server/tests/client.spec.ts @@ -1,9 +1,15 @@ import type * as http from "node:http"; -import type { DataType, Network, TaskProvider } from "@epfml/discojs"; +import type { + DataType, + Network, + TaskProvider, + ModelCard, +} from "@epfml/discojs"; import { aggregator as aggregators, client as clients, defaultTasks, + defaultModels, } from "@epfml/discojs"; import { afterEach, describe, expect, it } from "vitest"; import { Server } from "../src/index.js"; @@ -11,9 +17,10 @@ import { Server } from "../src/index.js"; describe("decentralized client", () => { let handle: http.Server; async function startServer( - ...tasks: TaskProvider[] + models: ModelCard[], + tasks: TaskProvider[], ): Promise { - const server = await Server.with(...tasks); + const server = await Server.with(models, tasks); let url: URL; [handle, url] = await server.serve(); @@ -30,7 +37,10 @@ describe("decentralized client", () => { ); it("connects to valid task", async () => { - const url = await startServer(defaultTasks.cifar10); + const url = await startServer( + [defaultModels.CIFAR10Classifier], + [defaultTasks.cifar10], + ); const client = new clients.decentralized.DecentralizedClient( url, @@ -43,7 +53,7 @@ describe("decentralized client", () => { }); it("fails to connect to invalid task", async () => { - const url = await startServer(); // no task + const url = await startServer([], []); // no models or tasks const client = new clients.decentralized.DecentralizedClient( url, @@ -58,9 +68,10 @@ describe("decentralized client", () => { describe("federated client", () => { let handle: http.Server; async function startServer( - ...tasks: TaskProvider[] + models: ModelCard[], + tasks: TaskProvider[], ): Promise { - const server = await Server.with(...tasks); + const server = await Server.with(models, tasks); let url: URL; [handle, url] = await server.serve(); @@ -77,7 +88,10 @@ describe("federated client", () => { ); it("connects to valid task", async () => { - const url = await startServer(defaultTasks.titanic); + const url = await startServer( + [defaultModels.TitanicClassifier], + [defaultTasks.titanic], + ); const client = new clients.federated.FederatedClient( url, @@ -90,7 +104,7 @@ describe("federated client", () => { }); it("fails to connect to invalid task", async () => { - const url = await startServer(); // no task + const url = await startServer([], []); // no task const client = new clients.federated.FederatedClient( url, diff --git a/server/tests/e2e/decentralized.spec.ts b/server/tests/e2e/decentralized.spec.ts index 9a1fcba99..87059efdc 100644 --- a/server/tests/e2e/decentralized.spec.ts +++ b/server/tests/e2e/decentralized.spec.ts @@ -1,10 +1,17 @@ import type * as http from "node:http"; -import type { DataType, RoundStatus, Task, TaskProvider } from "@epfml/discojs"; +import type { + DataType, + RoundStatus, + Task, + TaskProvider, + ModelCard, +} from "@epfml/discojs"; import { aggregator as aggregators, client as clients, Disco, defaultTasks, + defaultModels, WeightsContainer, } from "@epfml/discojs"; import { List } from "immutable"; @@ -32,9 +39,10 @@ async function expectWSToBeClose( describe("end-to-end decentralized", { timeout: 50_000 }, () => { let handle: http.Server | undefined; async function startServer( + model: ModelCard, task: TaskProvider, ): Promise { - const server = await Server.with(task); + const server = await Server.with([model], [task]); let url: URL; [handle, url] = await server.serve(); @@ -123,22 +131,34 @@ describe("end-to-end decentralized", { timeout: 50_000 }, () => { } it("single round of cifar 10 with three mean aggregators yields consensus", async () => { - const url = await startServer(defaultTasks.cifar10); + const url = await startServer( + defaultModels.CIFAR10Classifier, + defaultTasks.cifar10, + ); await reachConsensus(url, "mean"); }); it("several rounds of cifar 10 with three mean aggregators yields consensus", async () => { - const url = await startServer(defaultTasks.cifar10); + const url = await startServer( + defaultModels.CIFAR10Classifier, + defaultTasks.cifar10, + ); await reachConsensus(url, "mean", 3); }); it("single round of cifar 10 with three secure aggregators yields consensus", async () => { - const url = await startServer(defaultTasks.cifar10); + const url = await startServer( + defaultModels.CIFAR10Classifier, + defaultTasks.cifar10, + ); await reachConsensus(url, "secure"); }); it("several rounds of cifar 10 with three secure aggregators yields consensus", async () => { - const url = await startServer(defaultTasks.cifar10); + const url = await startServer( + defaultModels.CIFAR10Classifier, + defaultTasks.cifar10, + ); await reachConsensus(url, "secure", 3); }); @@ -154,10 +174,11 @@ describe("end-to-end decentralized", { timeout: 50_000 }, () => { minNbOfParticipants: 2, }, }; - const url = await startServer({ + const taskProvider = { ...defaultTasks.lusCovid, getTask: () => Promise.resolve(task), - }); + }; + const url = await startServer(defaultModels.LUSClassifier, taskProvider); const dataset = await datasets.loadLusCOVID(); /** diff --git a/server/tests/e2e/federated.spec.ts b/server/tests/e2e/federated.spec.ts index 21869a350..042d83cee 100644 --- a/server/tests/e2e/federated.spec.ts +++ b/server/tests/e2e/federated.spec.ts @@ -9,7 +9,7 @@ import type { TaskProvider, WeightsContainer, } from "@epfml/discojs"; -import { Disco, defaultTasks, defaultModels } from "@epfml/discojs"; +import { Disco, defaultTasks, defaultModels, ModelCard } from "@epfml/discojs"; import { List } from "immutable"; import { assert, afterEach, describe, expect, it } from "vitest"; import { Server } from "../../src/index.js"; @@ -32,9 +32,10 @@ async function arrayFromAsync(iter: AsyncIterable): Promise { describe("end-to-end federated", () => { let handle: http.Server | undefined; async function startServer( + model: ModelCard, task: TaskProvider, ): Promise { - const server = await Server.with(task); + const server = await Server.with([model], [task]); let url: URL; [handle, url] = await server.serve(); @@ -82,10 +83,14 @@ describe("end-to-end federated", () => { minNbOfParticipants: 2, }, }; - const url = await startServer({ + const cifar10TaskProvider = { getTask: () => Promise.resolve(cifar10Task), modelCard: defaultModels.CIFAR10Classifier, - }); + }; + const url = await startServer( + defaultModels.CIFAR10Classifier, + cifar10TaskProvider, + ); const dataset = await datasets.loadCifar10(); const [[m1, l1], [m2, l2], [m3, l3]] = await Promise.all([ @@ -107,10 +112,14 @@ describe("end-to-end federated", () => { ...task.trainingInformation, minNbOfParticipants: 2, }; - const url = await startServer({ + const taskProvider = { ...defaultTasks.titanic, getTask: () => Promise.resolve(task), - }); + }; + const url = await startServer( + defaultModels.TitanicClassifier, + taskProvider, + ); const dataset = datasets.loadTitanic(); const [[m1, l1], [m2, l2]] = await Promise.all([ @@ -133,10 +142,11 @@ describe("end-to-end federated", () => { roundDuration: 2, minNbOfParticipants: 2, }; - const url = await startServer({ + const taskProvider = { ...defaultTasks.lusCovid, getTask: () => Promise.resolve(task), - }); + }; + const url = await startServer(defaultModels.LUSClassifier, taskProvider); const dataset = await datasets.loadLusCOVID(); const [[m1, l1], [m2, l2]] = await Promise.all([ @@ -159,10 +169,11 @@ describe("end-to-end federated", () => { roundDuration: 2, minNbOfParticipants: 2, }; - const url = await startServer({ + const taskProvider = { ...defaultTasks.wikitext, getTask: () => Promise.resolve(task), - }); + }; + const url = await startServer(defaultModels.Wikitext, taskProvider); const dataset = datasets.loadWikitext(); const [r1, r2] = await Promise.all([ @@ -179,10 +190,11 @@ describe("end-to-end federated", () => { roundDuration: 1, minNbOfParticipants: 2, }; - const url = await startServer({ + const taskProvider = { ...defaultTasks.lusCovid, getTask: () => Promise.resolve(task), - }); + }; + const url = await startServer(defaultModels.LUSClassifier, taskProvider); const dataset = await datasets.loadLusCOVID(); /** @@ -337,10 +349,11 @@ describe("end-to-end federated", () => { }, }, }; - const url = await startServer({ + const taskProvider = { ...defaultTasks.lusCovid, getTask: () => Promise.resolve(task), - }); + }; + const url = await startServer(defaultModels.LUSClassifier, taskProvider); const dataset = await datasets.loadLusCOVID(); const [[m1, l1], [m2, l2], [m3, l3]] = await Promise.all([