@ariacompute/engine-rn 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -6
  2. package/package.json +1 -1
  3. package/src/index.js +211 -24
package/README.md CHANGED
@@ -12,12 +12,16 @@ npm publish name: `@ariacompute/engine-rn`. Device-farm CI: `.github/workflows/b
12
12
 
13
13
  `AriaEngine.open(modelRef, { token, site })` accepts a local bundle path **or**
14
14
  an Aria model name (e.g. `gemma-4-e2b-it_q4`). A value containing `/` or already
15
- on disk is loaded directly; otherwise the SDK downloads it from the Dashboard
16
- private source (requires `token`; `site` defaults to `https://ariacompute.com`)
17
- into `~/.ariacompute/models/{model}` and then loads it. A valid cached bundle is
18
- reused without re-downloading. The zip streaming + extraction is delegated to the
19
- native `AriaEngineModule` bridge.
15
+ on disk is loaded directly; otherwise the SDK downloads it from the regional
16
+ public hub (same as `aria-engine download`: `.com` → Hugging Face, `.cn` →
17
+ ModelScope; `site` defaults to `https://ariacompute.com`) into
18
+ `~/.ariacompute/models/{model}` and then loads it. Dashboard is not used. A
19
+ Dashboard `sk-` / `bfvk-` token is ignored for hub auth. Token is optional for
20
+ public models. Gated files: pass `hfToken` / `modelscopeApiToken` (same as
21
+ `aria-engine auth`); if omitted, reads `~/.ariacompute/config.yml`. A valid
22
+ cached bundle is reused without re-downloading.
20
23
 
21
24
  ```js
22
- const eng = await AriaEngine.open("gemma-4-e2b-it_q4", { token: "API_TOKEN" });
25
+ const eng = await AriaEngine.open("gemma-4-e2b-it_q4");
26
+ const gated = await AriaEngine.open("gemma-4-e2b-it_q4", { hfToken: "hf_..." });
23
27
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariacompute/engine-rn",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "React Native binding for Aria Engine",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -3,15 +3,25 @@
3
3
  * JS API mirrors @ariacompute/engine-ts for docs samples.
4
4
  *
5
5
  * Auto-download: `AriaEngine.open(modelRef, { token, site })` resolves a model
6
- * name via the Dashboard private source (requires `token`), downloads the zip,
7
- * extracts it through the native `AriaEngineModule` (which bridges unzip and
8
- * writes to `~/.ariacompute/models/{model}`), then loads via the native init.
6
+ * name via the regional public hub (Hugging Face / ModelScope — same as
7
+ * `aria-engine download`; Dashboard is not used), writes files to
8
+ * `~/.ariacompute/models/{model}`, then loads via native init.
9
9
  */
10
- import { NativeModules, Platform } from 'react-native';
10
+ import { NativeModules } from 'react-native';
11
11
 
12
12
  const AriaEngineModule = NativeModules.AriaEngine;
13
13
 
14
14
  const DEFAULT_SITE = 'https://ariacompute.com';
15
+ const DEFAULT_SDK = 'v1.0';
16
+ const HUB_REQUIRED = ['config.json', 'weight.bin'];
17
+ const HUB_OPTIONAL = [
18
+ 'tokenizer.json',
19
+ 'tokenizer.model',
20
+ 'tokenizer_config.json',
21
+ 'special_tokens_map.json',
22
+ 'vocab.json',
23
+ 'merges.txt',
24
+ ];
15
25
 
16
26
  function parseBundleName(model) {
17
27
  if (!model || model.includes('/') || model.includes('\\')) {
@@ -20,7 +30,10 @@ function parseBundleName(model) {
20
30
  const idx = model.lastIndexOf('_q');
21
31
  if (idx !== -1) {
22
32
  const slug = model.slice(0, idx);
23
- const suffix = model.slice(idx + 2);
33
+ let suffix = model.slice(idx + 2);
34
+ if (suffix.endsWith('_channel') || suffix.endsWith('_group')) {
35
+ suffix = suffix.slice(0, suffix.lastIndexOf('_'));
36
+ }
24
37
  const quant =
25
38
  suffix === '4'
26
39
  ? 'int4'
@@ -50,25 +63,202 @@ function isLocalRef(ref) {
50
63
  return ref.includes('/') || ref.includes('\\') || require('fs').existsSync(ref);
51
64
  }
52
65
 
53
- async function downloadModel(model, token, site = DEFAULT_SITE) {
54
- if (!token) throw new Error('api token is required to download a model');
55
- const { slug, quant } = parseBundleName(model);
66
+ function preferredPublicHub(site) {
67
+ if (site && String(site).toLowerCase().includes('ariacompute.cn')) return 'modelscope';
68
+ return 'huggingface';
69
+ }
70
+
71
+ function hubBearer(token) {
72
+ const t = (token || '').trim();
73
+ if (!t) return undefined;
74
+ const low = t.toLowerCase();
75
+ if (low.startsWith('sk-') || low.startsWith('bfvk-')) return undefined;
76
+ return t;
77
+ }
78
+
79
+ function unquoteYaml(v) {
80
+ const t = (v || '').trim();
81
+ if (t.length >= 2 && ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")))) {
82
+ return t.slice(1, -1);
83
+ }
84
+ return t;
85
+ }
86
+
87
+ function configYmlScalar(key) {
88
+ const fs = require('fs');
89
+ const path = require('path');
90
+ try {
91
+ const raw = fs.readFileSync(path.join(ariaHome(), 'config.yml'), 'utf8');
92
+ for (const line of raw.split('\n')) {
93
+ if (line.startsWith(' ') || line.startsWith('\t')) continue;
94
+ const s = line.trim();
95
+ if (!s || s.startsWith('#') || !s.includes(':')) continue;
96
+ const idx = s.indexOf(':');
97
+ if (s.slice(0, idx).trim() !== key) continue;
98
+ const v = unquoteYaml(s.slice(idx + 1));
99
+ return v || undefined;
100
+ }
101
+ } catch {
102
+ return undefined;
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ function resolveHubToken(source, opts = {}) {
108
+ const named = source === 'modelscope' ? opts.modelscopeApiToken : opts.hfToken;
109
+ const field = source === 'modelscope' ? 'modelscope_api_token' : 'hf_token';
110
+ for (const cand of [named, opts.token, configYmlScalar(field)]) {
111
+ const b = hubBearer(cand);
112
+ if (b) return b;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ function hubPathNames(model) {
118
+ const names = [model];
119
+ let lower = model.toLowerCase();
120
+ let core = model;
121
+ for (const suf of ['_channel', '_group']) {
122
+ if (lower.endsWith(suf)) {
123
+ core = model.slice(0, -suf.length);
124
+ lower = core.toLowerCase();
125
+ break;
126
+ }
127
+ }
128
+ const stems = [core];
129
+ if (lower.endsWith('_q326')) stems.push(`${core.slice(0, -5)}q3.26`);
130
+ else if (lower.endsWith('_q3.26')) stems.push(`${core.slice(0, -6)}q326`);
131
+ for (const stem of stems) {
132
+ for (const share of ['', '_channel', '_group']) {
133
+ const cand = `${stem}${share}`;
134
+ if (!names.includes(cand)) names.push(cand);
135
+ }
136
+ }
137
+ return names;
138
+ }
139
+
140
+ function hubFileUrls(source, model, file) {
141
+ const urls = [];
142
+ for (const name of hubPathNames(model)) {
143
+ if (source === 'modelscope') {
144
+ for (const repo of [`AriaCompute/${name}`, 'AriaCompute/model']) {
145
+ urls.push(`https://www.modelscope.cn/models/${repo}/resolve/master/${DEFAULT_SDK}/${name}/${file}`);
146
+ urls.push(`https://modelscope.cn/models/${repo}/resolve/master/${DEFAULT_SDK}/${name}/${file}`);
147
+ }
148
+ } else {
149
+ for (const repo of [`ariacompute/${name}`, 'ariacompute/model']) {
150
+ urls.push(`https://huggingface.co/${repo}/resolve/main/${DEFAULT_SDK}/${name}/${file}`);
151
+ }
152
+ }
153
+ }
154
+ return urls;
155
+ }
156
+
157
+ function isValidBundleSync(dir) {
158
+ const fs = require('fs');
159
+ const path = require('path');
160
+ try {
161
+ if (!fs.statSync(path.join(dir, 'weight.bin')).isFile()) return false;
162
+ const meta = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf8'));
163
+ return meta.format === 'aria-quant-bundle';
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ async function isValidBundle(dir) {
170
+ if (AriaEngineModule?.isValidBundle) {
171
+ try {
172
+ return await AriaEngineModule.isValidBundle(dir);
173
+ } catch {
174
+ /* fall through */
175
+ }
176
+ }
177
+ return isValidBundleSync(dir);
178
+ }
179
+
180
+ async function fetchUrlToFile(url, dest, token) {
181
+ const headers = {};
182
+ if (token) headers.Authorization = `Bearer ${token}`;
183
+ const resp = await fetch(url, { headers });
184
+ if (resp.status === 401 || resp.status === 403) {
185
+ const err = new Error(`HTTP ${resp.status}`);
186
+ err.status = resp.status;
187
+ throw err;
188
+ }
189
+ if (!resp.ok) {
190
+ const err = new Error(`HTTP ${resp.status}`);
191
+ err.status = resp.status;
192
+ throw err;
193
+ }
194
+ const fs = require('fs');
195
+ const path = require('path');
196
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
197
+ const buf = Buffer.from(await resp.arrayBuffer());
198
+ fs.writeFileSync(dest, buf);
199
+ }
200
+
201
+ async function fetchHubFile(source, model, file, dest, token, required) {
202
+ let last;
203
+ for (const url of hubFileUrls(source, model, file)) {
204
+ try {
205
+ await fetchUrlToFile(url, dest, token);
206
+ return true;
207
+ } catch (e) {
208
+ last = e;
209
+ if (e && (e.status === 401 || e.status === 403)) {
210
+ const field = source === 'modelscope' ? 'modelscope_api_token' : 'hf_token';
211
+ throw new Error(
212
+ `auth failed HTTP ${e.status}; set ${field} via aria-engine auth (do not pass a Dashboard sk-/bfvk- key as the hub token)`,
213
+ );
214
+ }
215
+ }
216
+ }
217
+ if (required) throw new Error(`${source}: missing ${file}${last ? `: ${last}` : ''}`);
218
+ return false;
219
+ }
220
+
221
+ async function downloadModel(model, tokenOrOpts, site = DEFAULT_SITE) {
222
+ const opts =
223
+ tokenOrOpts && typeof tokenOrOpts === 'object'
224
+ ? tokenOrOpts
225
+ : { token: tokenOrOpts, site };
226
+ parseBundleName(model);
227
+ const fs = require('fs');
228
+ const path = require('path');
229
+ const resolvedSite = opts.site ?? site ?? DEFAULT_SITE;
230
+ const source = preferredPublicHub(resolvedSite);
231
+ const hubToken = resolveHubToken(source, opts);
56
232
  const cache = cacheDir(model);
57
- if (require('fs').existsSync(cache) && (await AriaEngineModule?.isValidBundle?.(cache))) {
233
+ if (fs.existsSync(cache) && (await isValidBundle(cache))) {
58
234
  return cache;
59
235
  }
60
- if (!AriaEngineModule?.downloadModel) {
61
- throw new Error('native AriaEngineModule.downloadModel bridge is not linked');
236
+ const staging = path.join(ariaHome(), 'models', `.${model}.partial`);
237
+ try {
238
+ if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
239
+ fs.mkdirSync(staging, { recursive: true });
240
+ for (const file of HUB_REQUIRED) {
241
+ await fetchHubFile(source, model, file, path.join(staging, file), hubToken, true);
242
+ }
243
+ for (const extra of HUB_OPTIONAL) {
244
+ try {
245
+ await fetchHubFile(source, model, extra, path.join(staging, extra), hubToken, false);
246
+ } catch {
247
+ /* optional tokenizer sidecar */
248
+ }
249
+ }
250
+ if (!(await isValidBundle(staging))) {
251
+ throw new Error(
252
+ `${source} fetch completed but bundle invalid (need weight.bin + aria-quant-bundle config.json)`,
253
+ );
254
+ }
255
+ if (fs.existsSync(cache)) fs.rmSync(cache, { recursive: true, force: true });
256
+ fs.renameSync(staging, cache);
257
+ return cache;
258
+ } catch (e) {
259
+ if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
260
+ throw e;
62
261
  }
63
- const metaUrl = `${site.replace(/\/$/, '')}/api/dashboard/models/${encodeURIComponent(
64
- slug,
65
- )}/download?quant=${encodeURIComponent(quant)}&sdk=v1.0&format=json`;
66
- const metaResp = await fetch(metaUrl, { headers: { Authorization: `Bearer ${token}` } });
67
- if (!metaResp.ok) throw new Error(`dashboard request failed: ${metaResp.status}`);
68
- const meta = await metaResp.json();
69
- if (!meta.url) throw new Error('dashboard meta returned empty url');
70
- // Delegates streaming + zip extraction (and bundle validation) to native.
71
- return AriaEngineModule.downloadModel(model, meta.url, token, cache);
72
262
  }
73
263
 
74
264
  export class AriaEngine {
@@ -79,10 +269,7 @@ export class AriaEngine {
79
269
 
80
270
  static async open(modelRef, opts = {}) {
81
271
  if (isLocalRef(modelRef)) return new AriaEngine(modelRef);
82
- if (!opts.token) {
83
- throw new Error(`model name '${modelRef}' requires an api token to download`);
84
- }
85
- const bundle = await downloadModel(modelRef, opts.token, opts.site ?? DEFAULT_SITE);
272
+ const bundle = await downloadModel(modelRef, opts);
86
273
  return new AriaEngine(bundle);
87
274
  }
88
275