dsh-router-laya 2.1.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.
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Download the dsh-router-laya judge checkpoint (~846 MB) into `weights/model/`.
4
+ *
5
+ * Zero npm dependencies (Node >= 18 builtins only: fetch, fs, crypto, stream). The checkpoint is
6
+ * NOT in the npm package -- 842 MB exceeds npm's limits and would re-download on every plugin
7
+ * update -- so this script runs once at setup time and again whenever the manifest gains files.
8
+ *
9
+ * node weights/fetch.mjs [--dry-run] [--dest <dir>] [--repo <id>] [--revision <rev>]
10
+ *
11
+ * Behaviour, in the order that matters when a network misbehaves:
12
+ * - Endpoints are tried in order: `$HF_ENDPOINT` (when set) -> `https://huggingface.co`
13
+ * -> `https://hf-mirror.com`. hf.co is unreachable on some networks (observed on CN
14
+ * networks) while the mirror is not, so the mirror is the default fallback, not a manual
15
+ * step (docs/marketplace-review.md §3.4).
16
+ * - Files land as `<dest>/<path>.part` and are streamed with HTTP Range on resume, so an
17
+ * interrupted 842 MB transfer continues instead of restarting.
18
+ * - Every finished file is sha256-verified against `weights/manifest.json`; a mismatch
19
+ * deletes the file and falls through to the next endpoint. A file already present AND
20
+ * hash-correct is skipped, which is what makes repeated setup runs cheap.
21
+ *
22
+ * The checkpoint is self-contained (docs/marketplace-review.md §3.4): `model.safetensors` holds
23
+ * every parameter including the ModernBERT-large encoder, and the tokenizer + encoder configs
24
+ * ship inside the same download, so setup needs exactly this one fetch -- never a base-model
25
+ * download on top.
26
+ */
27
+ import crypto from 'node:crypto';
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ import { Readable } from 'node:stream';
31
+ import { Transform } from 'node:stream';
32
+ import { pipeline } from 'node:stream/promises';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ const here = path.dirname(fileURLToPath(import.meta.url));
36
+ const DEFAULT_ENDPOINTS = ['https://huggingface.co', 'https://hf-mirror.com'];
37
+ const DEFAULT_DEST = path.join(here, 'model');
38
+
39
+ // --- argument parsing (small on purpose; this runs once per machine) -------------------------
40
+
41
+ const args = process.argv.slice(2);
42
+ function argValue(flag) {
43
+ const i = args.indexOf(flag);
44
+ return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
45
+ }
46
+ const dryRun = args.includes('--dry-run');
47
+ const help = args.includes('--help') || args.includes('-h');
48
+
49
+ if (help) {
50
+ console.log(`usage: node weights/fetch.mjs [--dry-run] [--dest <dir>] [--repo <id>] [--revision <rev>]
51
+
52
+ --dry-run print the download plan (endpoints, files, hashes) and exit
53
+ --dest <dir> download target (default: <package>/weights/model)
54
+ --repo <id> Hugging Face repo id (default: manifest's "repo" field)
55
+ --revision <rev> branch/tag/commit (default: manifest's "revision" field)
56
+
57
+ env: HF_ENDPOINT (tried first, before the built-in endpoints)`);
58
+ process.exit(0);
59
+ }
60
+
61
+ // --- plan -------------------------------------------------------------------------------------
62
+
63
+ const manifest = JSON.parse(fs.readFileSync(path.join(here, 'manifest.json'), 'utf8'));
64
+ const repo = argValue('--repo') || process.env.LAYA_WEIGHTS_REPO || manifest.repo;
65
+ if (!repo) {
66
+ console.error('fetch: no repo id -- pass --repo <id> or set "repo" in weights/manifest.json');
67
+ process.exit(2);
68
+ }
69
+ const revision = argValue('--revision') || manifest.revision || 'main';
70
+ const dest = path.resolve(argValue('--dest') || DEFAULT_DEST);
71
+
72
+ // GitHub Releases is tried FIRST when the manifest declares a release base (the project's own
73
+ // distribution channel: versioned, no auth for public repos, GitHub-native). Each manifest file is
74
+ // fetched as `<releaseBase>/<file>`; failure falls through to the HF chain below. The HF chain
75
+ // remains the portable fallback (HF_ENDPOINT -> hf.co -> hf-mirror.com).
76
+ const releaseBase = (manifest.release_base || '').replace(/\/+$/, '');
77
+
78
+ // `$HF_ENDPOINT` first when set, then the built-ins, deduplicated in order.
79
+ const endpoints = [...new Set(
80
+ [process.env.HF_ENDPOINT, ...DEFAULT_ENDPOINTS].filter(Boolean).map((s) => s.replace(/\/+$/, '')),
81
+ )];
82
+
83
+ function fileUrl(endpoint, relPath) {
84
+ return `${endpoint}/${repo}/resolve/${revision}/${relPath.split('/').map(encodeURIComponent).join('/')}`;
85
+ }
86
+
87
+ function sha256File(file) {
88
+ return new Promise((resolve, reject) => {
89
+ const hash = crypto.createHash('sha256');
90
+ fs.createReadStream(file)
91
+ .on('data', (chunk) => hash.update(chunk))
92
+ .on('end', () => resolve(hash.digest('hex')))
93
+ .on('error', reject);
94
+ });
95
+ }
96
+
97
+ /** Download one file with `.part` resume; resolves when the part file is complete. */
98
+ async function downloadFile(url, relPath, bytes) {
99
+ const part = path.join(dest, relPath + '.part');
100
+ await fs.promises.mkdir(path.dirname(part), { recursive: true });
101
+ let start = fs.existsSync(part) ? fs.statSync(part).size : 0;
102
+ if (start > bytes) {
103
+ fs.rmSync(part); // a stale part from an older manifest is worse than none
104
+ start = 0;
105
+ }
106
+
107
+ const headers = start > 0 ? { Range: `bytes=${start}-` } : {};
108
+ const res = await fetch(url, { headers, redirect: 'follow' });
109
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
110
+ if (res.status === 200 && start > 0) {
111
+ start = 0; // server ignored the Range header; restart rather than corrupt
112
+ }
113
+ const expectedStatus = start > 0 ? 206 : 200;
114
+ if (res.status !== expectedStatus) {
115
+ throw new Error(`HTTP ${res.status} (wanted ${expectedStatus})`);
116
+ }
117
+
118
+ // Progress line every ~64 MB with a crude ETA (good enough for an 842 MB one-off).
119
+ let received = start;
120
+ let nextMark = Math.floor(received / 67108864) * 67108864 + 67108864;
121
+ const t0 = Date.now();
122
+ const counter = new Transform({
123
+ transform(chunk, _enc, cb) {
124
+ received += chunk.length;
125
+ if (received >= nextMark) {
126
+ nextMark = received + 67108864;
127
+ const rate = (received - start) / Math.max(1, Date.now() - t0); // B/ms == MB/s
128
+ const etaMin = rate > 0 ? ((bytes - received) / rate / 60000).toFixed(1) : '?';
129
+ process.stdout.write(` ${(received / 1e6).toFixed(0)}/${(bytes / 1e6).toFixed(0)} MB eta ~${etaMin} min\r`);
130
+ }
131
+ cb(null, chunk);
132
+ },
133
+ });
134
+ await pipeline(Readable.fromWeb(res.body), counter, fs.createWriteStream(part, { flags: start > 0 ? 'a' : 'w' }));
135
+ process.stdout.write('\n');
136
+
137
+ const size = fs.statSync(part).size;
138
+ if (size !== bytes) {
139
+ throw new Error(`incomplete: ${size}/${bytes} bytes`);
140
+ }
141
+ fs.renameSync(part, path.join(dest, relPath));
142
+ }
143
+
144
+ // --- run --------------------------------------------------------------------------------------
145
+
146
+ console.log(`laya-router-7q weights -> ${dest}`);
147
+ console.log(`repo: ${repo}@${revision}`);
148
+ console.log('endpoint order (first failure falls through to the next):');
149
+ let epLog = [...endpoints];
150
+ if (releaseBase) epLog = [`github-release: ${releaseBase}`, ...epLog];
151
+ epLog.forEach((ep, i) => console.log(` ${i + 1}. ${ep}`));
152
+ console.log(`files: ${manifest.files.length} (${(manifest.totalBytes / 1e6).toFixed(1)} MB total)`);
153
+
154
+ if (dryRun) {
155
+ for (const f of manifest.files) {
156
+ const local = path.join(dest, f.path);
157
+ const present = fs.existsSync(local) && fs.statSync(local).size === f.bytes ? 'present, will hash-verify' : 'missing, will download';
158
+ console.log(` ${f.path} ${f.bytes} B sha256=${f.sha256.slice(0, 12)}... [${present}]`);
159
+ console.log(` from ${releaseBase ? releaseBase + '/' + f.path + ' then fallbacks' : fileUrl(endpoints[0], f.path) + ' then fallbacks'}`);
160
+ }
161
+ console.log('dry-run: nothing downloaded.');
162
+ process.exit(0);
163
+ }
164
+
165
+ const failures = [];
166
+ for (const f of manifest.files) {
167
+ const target = path.join(dest, f.path);
168
+ if (fs.existsSync(target)) {
169
+ const got = await sha256File(target);
170
+ if (got === f.sha256) {
171
+ console.log(`ok (cached): ${f.path}`);
172
+ continue;
173
+ }
174
+ console.log(`hash mismatch on existing ${f.path} -- refetching`);
175
+ fs.rmSync(target);
176
+ }
177
+
178
+ // Source chain: GitHub Release (project's own channel) first when declared, then the HF chain.
179
+ // Release asset names are FLAT (GitHub forbids '/' in asset names): `encoder/config.json` is
180
+ // uploaded as `encoder__config.json`, so the release source maps manifest paths accordingly.
181
+ const assetName = f.path.split('/').join('__');
182
+ const sources = [
183
+ ...(releaseBase ? [{ label: 'github-release', url: `${releaseBase}/${assetName}` }] : []),
184
+ ...endpoints.map((ep) => ({ label: ep, url: fileUrl(ep, f.path) })),
185
+ ];
186
+ let done = false;
187
+ let lastError;
188
+ for (const src of sources) {
189
+ const url = src.url;
190
+ try {
191
+ console.log(`fetch ${f.path} from ${src.label} ...`);
192
+ await downloadFile(url, f.path, f.bytes);
193
+ const got = await sha256File(target);
194
+ if (got !== f.sha256) {
195
+ fs.rmSync(target);
196
+ throw new Error(`sha256 mismatch (got ${got.slice(0, 12)}..., want ${f.sha256.slice(0, 12)}...)`);
197
+ }
198
+ console.log(`ok: ${f.path} (${(f.bytes / 1e6).toFixed(1)} MB, sha256 verified)`);
199
+ done = true;
200
+ break;
201
+ } catch (e) {
202
+ lastError = e;
203
+ console.log(` failed: ${e.message} -- trying next endpoint`);
204
+ }
205
+ }
206
+ if (!done) {
207
+ failures.push({ file: f.path, error: lastError ? lastError.message : 'all endpoints failed' });
208
+ }
209
+ }
210
+
211
+ if (failures.length > 0) {
212
+ console.error('\nfetch FAILED for:');
213
+ for (const f of failures) console.error(` ${f.file}: ${f.error}`);
214
+ console.error(`\nmanual fallback: place the files under ${dest} yourself (names + sha256 in`);
215
+ console.error('weights/manifest.json), or retry with another endpoint:');
216
+ console.error(' HF_ENDPOINT=https://your-mirror node weights/fetch.mjs');
217
+ process.exit(1);
218
+ }
219
+ console.log('all weights present and verified.');
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "laya-router-7q",
3
+ "description": "dsh-router-laya judge checkpoint (7-question fine-tune of convaiinnovations/laya over answerdotai/ModernBERT-large). Hashes computed from training/laya_router_finetuned in the source repository.",
4
+ "repo": "HapyRain/laya-router-7q",
5
+ "revision": "main",
6
+ "release_base": "https://github.com/HapyRain/dsh-router-laya/releases/download/v2.1.0",
7
+ "license": "Apache-2.0",
8
+ "totalBytes": 846228407,
9
+ "files": [
10
+ {
11
+ "path": "model.safetensors",
12
+ "bytes": 842609220,
13
+ "sha256": "f14cf869ebc4d0adab3102627a92f0b677678d05e5353e3e55dc11f4fc6172de"
14
+ },
15
+ {
16
+ "path": "encoder/config.json",
17
+ "bytes": 2168,
18
+ "sha256": "eab8648c4b538efc2cb0c07ebef30b92de0dc596d0dc1818e202006958776289"
19
+ },
20
+ {
21
+ "path": "rl_agent_config.json",
22
+ "bytes": 811,
23
+ "sha256": "b6b5be8c3ebf051fc0b400f120d9f75dd4015f8488efd132e431da3f55c62d7d"
24
+ },
25
+ {
26
+ "path": "tokenizer/tokenizer.json",
27
+ "bytes": 3583228,
28
+ "sha256": "6c8aaa9a542084f2457eab775d4eeb51f92a70c0fd9de28d5edb0ddec3c08d30"
29
+ },
30
+ {
31
+ "path": "tokenizer/tokenizer_config.json",
32
+ "bytes": 350,
33
+ "sha256": "e07bcb74963b20b1193332afd3541b36dea30db1e4183d4002d739d5a4dfd5d2"
34
+ }
35
+ ]
36
+ }