@octomil/browser 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +202 -0
- package/dist/cache.js.map +1 -0
- package/dist/device-auth.d.ts +41 -0
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +203 -0
- package/dist/device-auth.js.map +1 -0
- package/dist/experiments.d.ts +44 -0
- package/dist/experiments.d.ts.map +1 -0
- package/dist/experiments.js +135 -0
- package/dist/experiments.js.map +1 -0
- package/dist/federated.d.ts +53 -0
- package/dist/federated.d.ts.map +1 -0
- package/dist/federated.js +180 -0
- package/dist/federated.js.map +1 -0
- package/dist/index.cjs +2148 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/inference.d.ts +43 -0
- package/dist/inference.d.ts.map +1 -0
- package/dist/inference.js +213 -0
- package/dist/inference.js.map +1 -0
- package/dist/integrity.d.ts +19 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +35 -0
- package/dist/integrity.js.map +1 -0
- package/dist/model-loader.d.ts +40 -0
- package/dist/model-loader.d.ts.map +1 -0
- package/dist/model-loader.js +232 -0
- package/dist/model-loader.js.map +1 -0
- package/dist/octomil.d.ts +92 -0
- package/dist/octomil.d.ts.map +1 -0
- package/dist/octomil.js +368 -0
- package/dist/octomil.js.map +1 -0
- package/dist/octomil.min.js +2849 -0
- package/dist/octomil.min.js.map +7 -0
- package/dist/privacy.d.ts +40 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +118 -0
- package/dist/privacy.js.map +1 -0
- package/dist/rollouts.d.ts +43 -0
- package/dist/rollouts.d.ts.map +1 -0
- package/dist/rollouts.js +114 -0
- package/dist/rollouts.js.map +1 -0
- package/dist/secure-aggregation.d.ts +50 -0
- package/dist/secure-aggregation.d.ts.map +1 -0
- package/dist/secure-aggregation.js +174 -0
- package/dist/secure-aggregation.js.map +1 -0
- package/dist/streaming.d.ts +25 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +148 -0
- package/dist/streaming.js.map +1 -0
- package/dist/telemetry.d.ts +41 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +130 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — A/B testing and experiments client
|
|
3
|
+
*
|
|
4
|
+
* Deterministic variant assignment, experiment config caching,
|
|
5
|
+
* and metric reporting for model experiments.
|
|
6
|
+
*/
|
|
7
|
+
import { OctomilError } from "./types.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// ExperimentsClient
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export class ExperimentsClient {
|
|
12
|
+
serverUrl;
|
|
13
|
+
apiKey;
|
|
14
|
+
cacheTtlMs;
|
|
15
|
+
onTelemetry;
|
|
16
|
+
experimentsCache = null;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.serverUrl = options.serverUrl;
|
|
19
|
+
this.apiKey = options.apiKey;
|
|
20
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 5 * 60 * 1000; // 5 min
|
|
21
|
+
this.onTelemetry = options.onTelemetry;
|
|
22
|
+
}
|
|
23
|
+
/** Fetch all active experiments (cached). */
|
|
24
|
+
async getActiveExperiments() {
|
|
25
|
+
if (this.experimentsCache &&
|
|
26
|
+
Date.now() - this.experimentsCache.fetchedAt < this.cacheTtlMs) {
|
|
27
|
+
return this.experimentsCache.experiments;
|
|
28
|
+
}
|
|
29
|
+
const url = `${this.serverUrl}/api/v1/experiments?status=active`;
|
|
30
|
+
const headers = { Accept: "application/json" };
|
|
31
|
+
if (this.apiKey)
|
|
32
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
33
|
+
const response = await fetch(url, { headers });
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new OctomilError("NETWORK_ERROR", `Failed to fetch experiments: HTTP ${response.status}`);
|
|
36
|
+
}
|
|
37
|
+
const data = (await response.json());
|
|
38
|
+
this.experimentsCache = {
|
|
39
|
+
experiments: data.experiments,
|
|
40
|
+
fetchedAt: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
return data.experiments;
|
|
43
|
+
}
|
|
44
|
+
/** Get full experiment config by ID. */
|
|
45
|
+
async getExperimentConfig(experimentId) {
|
|
46
|
+
const url = `${this.serverUrl}/api/v1/experiments/${encodeURIComponent(experimentId)}`;
|
|
47
|
+
const headers = { Accept: "application/json" };
|
|
48
|
+
if (this.apiKey)
|
|
49
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
50
|
+
const response = await fetch(url, { headers });
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new OctomilError("NETWORK_ERROR", `Failed to fetch experiment: HTTP ${response.status}`);
|
|
53
|
+
}
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deterministic variant assignment.
|
|
58
|
+
* Hash(deviceId + experimentId) → bucket → variant by cumulative traffic %.
|
|
59
|
+
*/
|
|
60
|
+
getVariant(experiment, deviceId) {
|
|
61
|
+
if (experiment.variants.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const bucket = deterministicBucket(deviceId + experiment.id);
|
|
64
|
+
let cumulative = 0;
|
|
65
|
+
for (const variant of experiment.variants) {
|
|
66
|
+
cumulative += variant.trafficPercentage;
|
|
67
|
+
if (bucket < cumulative) {
|
|
68
|
+
return variant;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Fallback to last variant if percentages don't sum to 100
|
|
72
|
+
return experiment.variants[experiment.variants.length - 1] ?? null;
|
|
73
|
+
}
|
|
74
|
+
/** Check if a device is enrolled in a specific experiment. */
|
|
75
|
+
isEnrolled(experiment, deviceId) {
|
|
76
|
+
return this.getVariant(experiment, deviceId) !== null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Find which experiment (if any) affects a given model, and return
|
|
80
|
+
* the variant this device should use.
|
|
81
|
+
*/
|
|
82
|
+
async resolveModelExperiment(modelId, deviceId) {
|
|
83
|
+
const experiments = await this.getActiveExperiments();
|
|
84
|
+
for (const exp of experiments) {
|
|
85
|
+
const affectsModel = exp.variants.some((v) => v.modelId === modelId);
|
|
86
|
+
if (!affectsModel)
|
|
87
|
+
continue;
|
|
88
|
+
const variant = this.getVariant(exp, deviceId);
|
|
89
|
+
if (variant) {
|
|
90
|
+
return { experiment: exp, variant };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/** Report a metric for an experiment. */
|
|
96
|
+
async trackMetric(experimentId, metricName, value, deviceId) {
|
|
97
|
+
const url = `${this.serverUrl}/api/v1/experiments/${encodeURIComponent(experimentId)}/metrics`;
|
|
98
|
+
const headers = {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
};
|
|
101
|
+
if (this.apiKey)
|
|
102
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
103
|
+
await fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
metric_name: metricName,
|
|
108
|
+
value,
|
|
109
|
+
device_id: deviceId,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
this.onTelemetry?.({
|
|
114
|
+
type: "experiment_metric",
|
|
115
|
+
model: experimentId,
|
|
116
|
+
metadata: { metricName, value },
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Clear the experiment cache. */
|
|
121
|
+
clearCache() {
|
|
122
|
+
this.experimentsCache = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
function deterministicBucket(input) {
|
|
129
|
+
let hash = 5381;
|
|
130
|
+
for (let i = 0; i < input.length; i++) {
|
|
131
|
+
hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0;
|
|
132
|
+
}
|
|
133
|
+
return Math.abs(hash) % 100;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=experiments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments.js","sourceRoot":"","sources":["../src/experiments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — Federated learning client
|
|
3
|
+
*
|
|
4
|
+
* Participates in federated training rounds: local weight extraction,
|
|
5
|
+
* delta computation, and update submission. Actual gradient computation
|
|
6
|
+
* is delegated to user-provided training hooks since ONNX Runtime Web
|
|
7
|
+
* has limited training support.
|
|
8
|
+
*/
|
|
9
|
+
import type { TrainingConfig, FederatedRound, WeightMap, TelemetryEvent } from "./types.js";
|
|
10
|
+
/** Extract and compare model weights stored as named Float32Arrays. */
|
|
11
|
+
export declare class WeightExtractor {
|
|
12
|
+
/**
|
|
13
|
+
* Compute element-wise delta between two weight maps.
|
|
14
|
+
* `delta = after - before`
|
|
15
|
+
*/
|
|
16
|
+
static computeDelta(before: WeightMap, after: WeightMap): WeightMap;
|
|
17
|
+
/** Apply a delta to weights: `result = weights + delta`. */
|
|
18
|
+
static applyDelta(weights: WeightMap, delta: WeightMap): WeightMap;
|
|
19
|
+
/** Compute L2 norm of a weight map (flattened). */
|
|
20
|
+
static l2Norm(weights: WeightMap): number;
|
|
21
|
+
}
|
|
22
|
+
export declare class FederatedClient {
|
|
23
|
+
private readonly serverUrl;
|
|
24
|
+
private readonly apiKey?;
|
|
25
|
+
private readonly deviceId;
|
|
26
|
+
private readonly onTelemetry?;
|
|
27
|
+
constructor(options: {
|
|
28
|
+
serverUrl: string;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
deviceId: string;
|
|
31
|
+
onTelemetry?: (event: TelemetryEvent) => void;
|
|
32
|
+
});
|
|
33
|
+
/** Fetch the current training round from the server. */
|
|
34
|
+
getTrainingRound(federationId: string): Promise<FederatedRound>;
|
|
35
|
+
/** Join a training round. */
|
|
36
|
+
joinRound(federationId: string, roundId: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Run local training using a user-provided step function.
|
|
39
|
+
*
|
|
40
|
+
* Browser ONNX Runtime Web does not support training natively, so the
|
|
41
|
+
* caller provides `onTrainStep` which receives the current weights and
|
|
42
|
+
* a batch of data, and returns updated weights.
|
|
43
|
+
*/
|
|
44
|
+
train(initialWeights: WeightMap, config: TrainingConfig): Promise<{
|
|
45
|
+
finalWeights: WeightMap;
|
|
46
|
+
delta: WeightMap;
|
|
47
|
+
}>;
|
|
48
|
+
/** Submit a weight update to the aggregation server. */
|
|
49
|
+
submitUpdate(federationId: string, roundId: string, delta: WeightMap, metrics?: Record<string, number>): Promise<void>;
|
|
50
|
+
private request;
|
|
51
|
+
private cloneWeights;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=federated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federated.d.ts","sourceRoot":"","sources":["../src/federated.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,SAAS,EACT,cAAc,EACf,MAAM,YAAY,CAAC;AAOpB,uEAAuE;AACvE,qBAAa,eAAe;IAC1B;;;OAGG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS;IAoBnE,4DAA4D;IAC5D,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS;IAmBlE,mDAAmD;IACnD,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM;CAU1C;AAMD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAkC;gBAEnD,OAAO,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;KAC/C;IAOD,wDAAwD;IAClD,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAarE,6BAA6B;IACvB,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBrE;;;;;;OAMG;IACG,KAAK,CACT,cAAc,EAAE,SAAS,EACzB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC;QAAE,YAAY,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IA8BzD,wDAAwD;IAClD,YAAY,CAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC;YAmCF,OAAO;IAcrB,OAAO,CAAC,YAAY;CAOrB"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — Federated learning client
|
|
3
|
+
*
|
|
4
|
+
* Participates in federated training rounds: local weight extraction,
|
|
5
|
+
* delta computation, and update submission. Actual gradient computation
|
|
6
|
+
* is delegated to user-provided training hooks since ONNX Runtime Web
|
|
7
|
+
* has limited training support.
|
|
8
|
+
*/
|
|
9
|
+
import { OctomilError } from "./types.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// WeightExtractor
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Extract and compare model weights stored as named Float32Arrays. */
|
|
14
|
+
export class WeightExtractor {
|
|
15
|
+
/**
|
|
16
|
+
* Compute element-wise delta between two weight maps.
|
|
17
|
+
* `delta = after - before`
|
|
18
|
+
*/
|
|
19
|
+
static computeDelta(before, after) {
|
|
20
|
+
const delta = {};
|
|
21
|
+
for (const key of Object.keys(before)) {
|
|
22
|
+
const b = before[key];
|
|
23
|
+
const a = after[key];
|
|
24
|
+
if (!b || !a || b.length !== a.length) {
|
|
25
|
+
throw new OctomilError("INVALID_INPUT", `Weight dimension mismatch for "${key}".`);
|
|
26
|
+
}
|
|
27
|
+
const d = new Float32Array(b.length);
|
|
28
|
+
for (let i = 0; i < b.length; i++) {
|
|
29
|
+
d[i] = a[i] - b[i];
|
|
30
|
+
}
|
|
31
|
+
delta[key] = d;
|
|
32
|
+
}
|
|
33
|
+
return delta;
|
|
34
|
+
}
|
|
35
|
+
/** Apply a delta to weights: `result = weights + delta`. */
|
|
36
|
+
static applyDelta(weights, delta) {
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const key of Object.keys(weights)) {
|
|
39
|
+
const w = weights[key];
|
|
40
|
+
const d = delta[key];
|
|
41
|
+
if (!w)
|
|
42
|
+
continue;
|
|
43
|
+
if (!d || w.length !== d.length) {
|
|
44
|
+
result[key] = new Float32Array(w);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const r = new Float32Array(w.length);
|
|
48
|
+
for (let i = 0; i < w.length; i++) {
|
|
49
|
+
r[i] = w[i] + d[i];
|
|
50
|
+
}
|
|
51
|
+
result[key] = r;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/** Compute L2 norm of a weight map (flattened). */
|
|
56
|
+
static l2Norm(weights) {
|
|
57
|
+
let sumSq = 0;
|
|
58
|
+
for (const arr of Object.values(weights)) {
|
|
59
|
+
if (!arr)
|
|
60
|
+
continue;
|
|
61
|
+
for (let i = 0; i < arr.length; i++) {
|
|
62
|
+
sumSq += arr[i] * arr[i];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Math.sqrt(sumSq);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// FederatedClient
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
export class FederatedClient {
|
|
72
|
+
serverUrl;
|
|
73
|
+
apiKey;
|
|
74
|
+
deviceId;
|
|
75
|
+
onTelemetry;
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.serverUrl = options.serverUrl;
|
|
78
|
+
this.apiKey = options.apiKey;
|
|
79
|
+
this.deviceId = options.deviceId;
|
|
80
|
+
this.onTelemetry = options.onTelemetry;
|
|
81
|
+
}
|
|
82
|
+
/** Fetch the current training round from the server. */
|
|
83
|
+
async getTrainingRound(federationId) {
|
|
84
|
+
const response = await this.request(`/api/v1/federations/${encodeURIComponent(federationId)}/rounds/current`);
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new OctomilError("NETWORK_ERROR", `Failed to fetch training round: HTTP ${response.status}`);
|
|
87
|
+
}
|
|
88
|
+
return (await response.json());
|
|
89
|
+
}
|
|
90
|
+
/** Join a training round. */
|
|
91
|
+
async joinRound(federationId, roundId) {
|
|
92
|
+
const response = await this.request(`/api/v1/federations/${encodeURIComponent(federationId)}/rounds/${encodeURIComponent(roundId)}/join`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: JSON.stringify({ device_id: this.deviceId }),
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new OctomilError("NETWORK_ERROR", `Failed to join round: HTTP ${response.status}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Run local training using a user-provided step function.
|
|
102
|
+
*
|
|
103
|
+
* Browser ONNX Runtime Web does not support training natively, so the
|
|
104
|
+
* caller provides `onTrainStep` which receives the current weights and
|
|
105
|
+
* a batch of data, and returns updated weights.
|
|
106
|
+
*/
|
|
107
|
+
async train(initialWeights, config) {
|
|
108
|
+
const start = performance.now();
|
|
109
|
+
let weights = this.cloneWeights(initialWeights);
|
|
110
|
+
for (let epoch = 0; epoch < config.epochs; epoch++) {
|
|
111
|
+
const stepResult = await config.onTrainStep(weights, {
|
|
112
|
+
epoch,
|
|
113
|
+
batchSize: config.batchSize,
|
|
114
|
+
learningRate: config.learningRate,
|
|
115
|
+
});
|
|
116
|
+
weights = stepResult.weights;
|
|
117
|
+
}
|
|
118
|
+
const delta = WeightExtractor.computeDelta(initialWeights, weights);
|
|
119
|
+
const durationMs = performance.now() - start;
|
|
120
|
+
this.onTelemetry?.({
|
|
121
|
+
type: "training_complete",
|
|
122
|
+
model: config.modelId ?? "unknown",
|
|
123
|
+
durationMs,
|
|
124
|
+
metadata: {
|
|
125
|
+
epochs: config.epochs,
|
|
126
|
+
deltaNorm: WeightExtractor.l2Norm(delta),
|
|
127
|
+
},
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
return { finalWeights: weights, delta };
|
|
131
|
+
}
|
|
132
|
+
/** Submit a weight update to the aggregation server. */
|
|
133
|
+
async submitUpdate(federationId, roundId, delta, metrics) {
|
|
134
|
+
// Serialize WeightMap to a transferable format
|
|
135
|
+
const serialized = {};
|
|
136
|
+
for (const [key, arr] of Object.entries(delta)) {
|
|
137
|
+
if (!arr)
|
|
138
|
+
continue;
|
|
139
|
+
serialized[key] = {
|
|
140
|
+
data: Array.from(arr),
|
|
141
|
+
shape: [arr.length],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const response = await this.request(`/api/v1/federations/${encodeURIComponent(federationId)}/rounds/${encodeURIComponent(roundId)}/submit`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
device_id: this.deviceId,
|
|
148
|
+
delta: serialized,
|
|
149
|
+
metrics,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new OctomilError("NETWORK_ERROR", `Failed to submit update: HTTP ${response.status}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
// Internal
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
async request(path, init) {
|
|
160
|
+
const headers = {
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
};
|
|
163
|
+
if (this.apiKey) {
|
|
164
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
165
|
+
}
|
|
166
|
+
return fetch(`${this.serverUrl}${path}`, {
|
|
167
|
+
...init,
|
|
168
|
+
headers: { ...headers, ...init?.headers },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
cloneWeights(weights) {
|
|
172
|
+
const cloned = {};
|
|
173
|
+
for (const [key, arr] of Object.entries(weights)) {
|
|
174
|
+
if (arr)
|
|
175
|
+
cloned[key] = new Float32Array(arr);
|
|
176
|
+
}
|
|
177
|
+
return cloned;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=federated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federated.js","sourceRoot":"","sources":["../src/federated.ts"],"names":[],"mappings":"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"}
|