@ariacompute/engine-ts 1.13.0 → 1.14.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/README.md CHANGED
@@ -16,11 +16,17 @@ eng.close();
16
16
 
17
17
  `Engine.open(modelRef, { token, site })` accepts a local bundle path **or** an
18
18
  Aria model name (e.g. `gemma-4-e2b-it_q4`). A value containing `/` or already on
19
- disk is loaded directly; otherwise the SDK downloads it from the Dashboard
20
- private source (requires `token`; `site` defaults to `https://ariacompute.com`)
21
- into `~/.ariacompute/models/{model}` and then loads it. A valid cached bundle is
22
- reused without re-downloading.
19
+ disk is loaded directly; otherwise the SDK downloads it from the regional public
20
+ hub (same as `aria-engine download`: `.com` → Hugging Face, `.cn` → ModelScope;
21
+ `site` defaults to `https://ariacompute.com`) into `~/.ariacompute/models/{model}`
22
+ and then loads it. Dashboard is not used. A Dashboard `sk-` / `bfvk-` token is
23
+ ignored for hub auth. Token is optional for public models. A valid cached bundle
24
+ is reused without re-downloading.
25
+
26
+ Gated files: pass `hfToken` (`.com`) or `modelscopeApiToken` (`.cn`) — same keys as
27
+ `aria-engine auth`. If omitted, the SDK reads `~/.ariacompute/config.yml`.
23
28
 
24
29
  ```ts
25
- const eng = await Engine.open("gemma-4-e2b-it_q4", { token: "API_TOKEN" });
30
+ const eng = await Engine.open("gemma-4-e2b-it_q4");
31
+ const gated = await Engine.open("gemma-4-e2b-it_q4", { hfToken: "hf_..." });
26
32
  ```
package/dist/index.d.ts CHANGED
@@ -27,9 +27,13 @@ export interface CompleteResult {
27
27
  generation?: Generation;
28
28
  }
29
29
  export interface OpenOptions {
30
- /** Dashboard bearer token. Required when modelRef is a model name. */
30
+ /** Legacy generic hub token. Dashboard sk-/bfvk- keys are ignored. */
31
31
  token?: string;
32
- /** Dashboard base URL. Defaults to https://ariacompute.com. */
32
+ /** Hugging Face hub token (`.com`). Same field as `aria-engine auth` `hf_token`. */
33
+ hfToken?: string;
34
+ /** ModelScope hub token (`.cn`). Same field as `aria-engine auth` `modelscope_api_token`. */
35
+ modelscopeApiToken?: string;
36
+ /** Site used to pick the regional hub. Defaults to https://ariacompute.com (.com → HF, .cn → ModelScope). */
33
37
  site?: string;
34
38
  /** Explicit path to the FFI library. */
35
39
  ffiLib?: string;
@@ -39,10 +43,16 @@ export declare function parseBundleName(model: string): {
39
43
  slug: string;
40
44
  quant: string;
41
45
  };
42
- /** Download `model` from the Dashboard private source into
46
+ export declare function preferredPublicHub(site?: string): "huggingface" | "modelscope";
47
+ export declare function hubBearer(token?: string): string | undefined;
48
+ export declare function configYmlScalar(key: string): string | undefined;
49
+ export declare function resolveHubToken(source: "huggingface" | "modelscope", opts?: Pick<OpenOptions, "token" | "hfToken" | "modelscopeApiToken">): string | undefined;
50
+ export declare function hubFileUrls(source: "huggingface" | "modelscope", model: string, file: string, sdk?: string): string[];
51
+ /** Download `model` from the regional public hub into
43
52
  * `~/.ariacompute/models/{model}` and return that directory.
44
- * Skips the download when a valid bundle is already cached. */
45
- export declare function downloadModel(model: string, token: string, site?: string): Promise<string>;
53
+ * Matches aria-engine download: .com → Hugging Face, .cn → ModelScope.
54
+ * Dashboard is not used. Skips the download when a valid bundle is already cached. */
55
+ export declare function downloadModel(model: string, tokenOrOpts?: string | OpenOptions, site?: string): Promise<string>;
46
56
  export declare function isLocalRef(modelRef: string): boolean;
47
57
  export declare class Engine {
48
58
  private lib;
@@ -56,8 +66,7 @@ export declare class Engine {
56
66
  /** Construct from a local bundle directory. */
57
67
  constructor(bundle: string, opts?: OpenOptions);
58
68
  /** Auto-detect: a value containing a separator or already on disk is a local
59
- * path; otherwise it is a model name that is downloaded (requires `token`)
60
- * then loaded. */
69
+ * path; otherwise it is a model name downloaded from the regional public hub. */
61
70
  static open(modelRef: string, opts?: OpenOptions): Promise<Engine>;
62
71
  complete(messages: Turn[], opts?: GenerateOptions): CompleteResult;
63
72
  embed(text: string): number[];
package/dist/index.js CHANGED
@@ -35,13 +35,29 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports._internal = exports.Engine = void 0;
37
37
  exports.parseBundleName = parseBundleName;
38
+ exports.preferredPublicHub = preferredPublicHub;
39
+ exports.hubBearer = hubBearer;
40
+ exports.configYmlScalar = configYmlScalar;
41
+ exports.resolveHubToken = resolveHubToken;
42
+ exports.hubFileUrls = hubFileUrls;
38
43
  exports.downloadModel = downloadModel;
39
44
  exports.isLocalRef = isLocalRef;
40
45
  const fs = __importStar(require("node:fs"));
41
46
  const os = __importStar(require("node:os"));
42
47
  const path = __importStar(require("node:path"));
43
- const fflate = __importStar(require("fflate"));
48
+ const promises_1 = require("node:stream/promises");
49
+ const node_stream_1 = require("node:stream");
44
50
  const DEFAULT_SITE = "https://ariacompute.com";
51
+ const DEFAULT_SDK = "v1.0";
52
+ const HUB_REQUIRED = ["config.json", "weight.bin"];
53
+ const HUB_OPTIONAL = [
54
+ "tokenizer.json",
55
+ "tokenizer.model",
56
+ "tokenizer_config.json",
57
+ "special_tokens_map.json",
58
+ "vocab.json",
59
+ "merges.txt",
60
+ ];
45
61
  function ariaHome() {
46
62
  return process.env.ARIA_COMPUTE_HOME || path.join(os.homedir(), ".ariacompute");
47
63
  }
@@ -56,7 +72,10 @@ function parseBundleName(model) {
56
72
  const idx = model.lastIndexOf("_q");
57
73
  if (idx !== -1) {
58
74
  const slug = model.slice(0, idx);
59
- const suffix = model.slice(idx + 2);
75
+ let suffix = model.slice(idx + 2);
76
+ if (suffix.endsWith("_channel") || suffix.endsWith("_group")) {
77
+ suffix = suffix.slice(0, suffix.lastIndexOf("_"));
78
+ }
60
79
  const quant = suffix === "4"
61
80
  ? "int4"
62
81
  : suffix === "8"
@@ -83,81 +102,185 @@ function isValidBundle(dir) {
83
102
  return false;
84
103
  }
85
104
  }
86
- function flattenSingleSubdir(dir) {
87
- const entries = fs.readdirSync(dir).filter((e) => !e.startsWith("."));
88
- if (entries.length !== 1)
89
- return;
90
- const only = path.join(dir, entries[0]);
91
- if (!fs.statSync(only).isDirectory())
92
- return;
93
- if (!fs.existsSync(path.join(only, "config.json")))
94
- return;
95
- for (const name of fs.readdirSync(only)) {
96
- fs.renameSync(path.join(only, name), path.join(dir, name));
105
+ function preferredPublicHub(site) {
106
+ if (site && site.toLowerCase().includes("ariacompute.cn"))
107
+ return "modelscope";
108
+ return "huggingface";
109
+ }
110
+ function hubBearer(token) {
111
+ const t = (token || "").trim();
112
+ if (!t)
113
+ return undefined;
114
+ const low = t.toLowerCase();
115
+ if (low.startsWith("sk-") || low.startsWith("bfvk-"))
116
+ return undefined;
117
+ return t;
118
+ }
119
+ function unquoteYaml(v) {
120
+ const t = v.trim();
121
+ if (t.length >= 2 && ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")))) {
122
+ return t.slice(1, -1);
123
+ }
124
+ return t;
125
+ }
126
+ function configYmlScalar(key) {
127
+ try {
128
+ const raw = fs.readFileSync(path.join(ariaHome(), "config.yml"), "utf8");
129
+ for (const line of raw.split("\n")) {
130
+ if (line.startsWith(" ") || line.startsWith("\t"))
131
+ continue;
132
+ const s = line.trim();
133
+ if (!s || s.startsWith("#") || !s.includes(":"))
134
+ continue;
135
+ const idx = s.indexOf(":");
136
+ if (s.slice(0, idx).trim() !== key)
137
+ continue;
138
+ const v = unquoteYaml(s.slice(idx + 1));
139
+ return v || undefined;
140
+ }
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ return undefined;
146
+ }
147
+ function resolveHubToken(source, opts = {}) {
148
+ const named = source === "modelscope" ? opts.modelscopeApiToken : opts.hfToken;
149
+ const field = source === "modelscope" ? "modelscope_api_token" : "hf_token";
150
+ for (const cand of [named, opts.token, configYmlScalar(field)]) {
151
+ const b = hubBearer(cand);
152
+ if (b)
153
+ return b;
97
154
  }
98
- fs.rmdirSync(only);
155
+ return undefined;
99
156
  }
100
- async function extractZip(data, dest) {
101
- if (data[0] !== 0x50 || data[1] !== 0x4b) {
102
- throw new Error("downloaded archive is not a valid zip");
157
+ function hubPathNames(model) {
158
+ const names = [model];
159
+ let lower = model.toLowerCase();
160
+ let core = model;
161
+ for (const suf of ["_channel", "_group"]) {
162
+ if (lower.endsWith(suf)) {
163
+ core = model.slice(0, -suf.length);
164
+ lower = core.toLowerCase();
165
+ break;
166
+ }
103
167
  }
104
- fs.mkdirSync(dest, { recursive: true });
105
- const entries = fflate.unzipSync(data);
106
- for (const [name, content] of Object.entries(entries)) {
107
- const out = path.join(dest, name);
108
- if (name.endsWith("/")) {
109
- fs.mkdirSync(out, { recursive: true });
168
+ const stems = [core];
169
+ if (lower.endsWith("_q326"))
170
+ stems.push(`${core.slice(0, -5)}q3.26`);
171
+ else if (lower.endsWith("_q3.26"))
172
+ stems.push(`${core.slice(0, -6)}q326`);
173
+ for (const stem of stems) {
174
+ for (const share of ["", "_channel", "_group"]) {
175
+ const cand = `${stem}${share}`;
176
+ if (!names.includes(cand))
177
+ names.push(cand);
178
+ }
179
+ }
180
+ return names;
181
+ }
182
+ function hubFileUrls(source, model, file, sdk = DEFAULT_SDK) {
183
+ const urls = [];
184
+ for (const name of hubPathNames(model)) {
185
+ if (source === "modelscope") {
186
+ for (const repo of [`AriaCompute/${name}`, "AriaCompute/model"]) {
187
+ urls.push(`https://www.modelscope.cn/models/${repo}/resolve/master/${sdk}/${name}/${file}`);
188
+ urls.push(`https://modelscope.cn/models/${repo}/resolve/master/${sdk}/${name}/${file}`);
189
+ }
110
190
  }
111
191
  else {
112
- fs.mkdirSync(path.dirname(out), { recursive: true });
113
- fs.writeFileSync(out, content);
192
+ for (const repo of [`ariacompute/${name}`, "ariacompute/model"]) {
193
+ urls.push(`https://huggingface.co/${repo}/resolve/main/${sdk}/${name}/${file}`);
194
+ }
114
195
  }
115
196
  }
116
- flattenSingleSubdir(dest);
197
+ return urls;
117
198
  }
118
- async function httpGet(url, token) {
119
- const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
199
+ class HubHttpError extends Error {
200
+ constructor(status, message) {
201
+ super(message);
202
+ this.status = status;
203
+ }
204
+ }
205
+ async function fetchUrlToFile(url, dest, token) {
206
+ const headers = {};
207
+ if (token)
208
+ headers.Authorization = `Bearer ${token}`;
209
+ const resp = await fetch(url, { headers });
210
+ if (resp.status === 401 || resp.status === 403) {
211
+ throw new HubHttpError(resp.status, `HTTP ${resp.status}`);
212
+ }
120
213
  if (!resp.ok)
121
- return { ok: false, status: resp.status, body: null };
122
- const ct = resp.headers.get("content-type") || "";
123
- if (ct.includes("json"))
124
- return { ok: true, status: resp.status, body: await resp.json() };
125
- return { ok: true, status: resp.status, body: Buffer.from(await resp.arrayBuffer()) };
214
+ throw new HubHttpError(resp.status, `HTTP ${resp.status}`);
215
+ if (!resp.body)
216
+ throw new Error("empty response body");
217
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
218
+ await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(resp.body), fs.createWriteStream(dest));
219
+ }
220
+ async function fetchHubFile(source, model, file, dest, token, required) {
221
+ let last;
222
+ for (const url of hubFileUrls(source, model, file)) {
223
+ try {
224
+ await fetchUrlToFile(url, dest, token);
225
+ return true;
226
+ }
227
+ catch (e) {
228
+ last = e;
229
+ if (e instanceof HubHttpError && (e.status === 401 || e.status === 403)) {
230
+ const field = source === "modelscope" ? "modelscope_api_token" : "hf_token";
231
+ throw new Error(`auth failed HTTP ${e.status}; set ${field} via aria-engine auth (do not pass a Dashboard sk-/bfvk- key as the hub token)`);
232
+ }
233
+ }
234
+ }
235
+ if (required)
236
+ throw new Error(`${source}: missing ${file}${last ? `: ${last}` : ""}`);
237
+ return false;
126
238
  }
127
239
  const encoder = new TextEncoder();
128
- /** Download `model` from the Dashboard private source into
240
+ /** Download `model` from the regional public hub into
129
241
  * `~/.ariacompute/models/{model}` and return that directory.
130
- * Skips the download when a valid bundle is already cached. */
131
- async function downloadModel(model, token, site = DEFAULT_SITE) {
132
- if (!token)
133
- throw new Error("api token is required to download a model");
134
- const { slug, quant } = parseBundleName(model);
242
+ * Matches aria-engine download: .com → Hugging Face, .cn → ModelScope.
243
+ * Dashboard is not used. Skips the download when a valid bundle is already cached. */
244
+ async function downloadModel(model, tokenOrOpts, site = DEFAULT_SITE) {
245
+ const opts = tokenOrOpts && typeof tokenOrOpts === "object"
246
+ ? tokenOrOpts
247
+ : { token: tokenOrOpts, site };
248
+ parseBundleName(model);
249
+ const resolvedSite = opts.site ?? site ?? DEFAULT_SITE;
250
+ const source = preferredPublicHub(resolvedSite);
251
+ const hubToken = resolveHubToken(source, opts);
135
252
  const cache = cacheDir(model);
136
253
  if (fs.existsSync(cache) && isValidBundle(cache))
137
254
  return cache;
138
- const metaUrl = `${site.replace(/\/$/, "")}/api/dashboard/models/${encodeURIComponent(slug)}/download?quant=${encodeURIComponent(quant)}&sdk=v1.0&format=json`;
139
- const metaResp = await httpGet(metaUrl, token);
140
- if (!metaResp.ok)
141
- throw new Error(`dashboard request failed: ${metaResp.status}`);
142
- const meta = metaResp.body;
143
- if (!meta.url)
144
- throw new Error("dashboard meta returned empty url");
145
- const zipResp = await httpGet(meta.url, token);
146
- if (!zipResp.ok)
147
- throw new Error(`download stream failed: ${zipResp.status}`);
148
- const data = zipResp.body;
149
255
  const staging = path.join(ariaHome(), "models", `.${model}.partial`);
150
- if (fs.existsSync(staging))
151
- fs.rmSync(staging, { recursive: true, force: true });
152
- await extractZip(data, staging);
153
- if (!isValidBundle(staging)) {
154
- fs.rmSync(staging, { recursive: true, force: true });
155
- throw new Error("downloaded archive did not contain a valid aria-quant-bundle");
156
- }
157
- if (fs.existsSync(cache))
158
- fs.rmSync(cache, { recursive: true, force: true });
159
- fs.renameSync(staging, cache);
160
- return cache;
256
+ try {
257
+ if (fs.existsSync(staging))
258
+ fs.rmSync(staging, { recursive: true, force: true });
259
+ fs.mkdirSync(staging, { recursive: true });
260
+ for (const file of HUB_REQUIRED) {
261
+ await fetchHubFile(source, model, file, path.join(staging, file), hubToken, true);
262
+ }
263
+ for (const extra of HUB_OPTIONAL) {
264
+ try {
265
+ await fetchHubFile(source, model, extra, path.join(staging, extra), hubToken, false);
266
+ }
267
+ catch {
268
+ /* optional tokenizer sidecar */
269
+ }
270
+ }
271
+ if (!isValidBundle(staging)) {
272
+ throw new Error(`${source} fetch completed but bundle invalid (need weight.bin + aria-quant-bundle config.json)`);
273
+ }
274
+ if (fs.existsSync(cache))
275
+ fs.rmSync(cache, { recursive: true, force: true });
276
+ fs.renameSync(staging, cache);
277
+ return cache;
278
+ }
279
+ catch (e) {
280
+ if (fs.existsSync(staging))
281
+ fs.rmSync(staging, { recursive: true, force: true });
282
+ throw e;
283
+ }
161
284
  }
162
285
  function loadLib(ffiLib) {
163
286
  const libPath = ffiLib || process.env.ARIA_FFI_LIB;
@@ -186,15 +309,11 @@ class Engine {
186
309
  }
187
310
  }
188
311
  /** Auto-detect: a value containing a separator or already on disk is a local
189
- * path; otherwise it is a model name that is downloaded (requires `token`)
190
- * then loaded. */
312
+ * path; otherwise it is a model name downloaded from the regional public hub. */
191
313
  static async open(modelRef, opts = {}) {
192
314
  if (isLocalRef(modelRef))
193
315
  return new Engine(modelRef, opts);
194
- if (!opts.token) {
195
- throw new Error(`model name '${modelRef}' requires an api token to download`);
196
- }
197
- const bundle = await downloadModel(modelRef, opts.token, opts.site ?? DEFAULT_SITE);
316
+ const bundle = await downloadModel(modelRef, opts);
198
317
  return new Engine(bundle, opts);
199
318
  }
200
319
  complete(messages, opts = {}) {
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "@ariacompute/engine-ts",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "TypeScript/Node SDK for Aria Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc -p .",
9
- "test": "node --test test/binding.test.mjs"
9
+ "test": "node --test test/binding.test.mjs test/download.test.mjs"
10
10
  },
11
11
  "license": "MIT",
12
12
  "dependencies": {
13
- "fflate": "^0.8.2",
14
13
  "koffi": "^2.8.0"
15
14
  },
16
15
  "devDependencies": {