laya-system-one 1.1.0 → 1.2.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
@@ -1,384 +1,471 @@
1
- # Laya System-One ⚡
2
-
3
- [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
- [![Runtime](https://img.shields.io/badge/Runtime-Node.js%20%7C%20Bun%20%7C%20Browser-green.svg)]()
5
- [![TypeSafe Jev](https://img.shields.io/badge/Wire%20Protocol-TypeSafe%20Jev%20Compatible-orange.svg)]()
6
- [![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)]()
7
-
8
- > **A fast, self-contained decision engine. Give it any text and a set of typed questions, and it answers them — offline, in milliseconds, in over 100 languages. Drop-in compatible with the TypeSafe Jev API (`POST /v1/systemone`).**
9
-
10
- Runs entirely on your machine. No Python, no PyTorch, no API keys, no cloud
11
- calls at inference time. One `npm install` and it works.
12
-
13
- ---
14
-
15
- ## 🌟 Why Laya System-One?
16
-
17
- - 🔒 **100% Offline:** Nothing leaves your machine. Ideal for corporate
18
- intranets, edge servers and privacy-sensitive workflows.
19
- - ⚡ **Fast:** ~40 ms per question on a warm engine, measured on every platform
20
- we ship for.
21
- - 🔄 **TypeSafe Jev Compatible:** Drop-in `POST /v1/systemone`. Point an
22
- existing Jev client at it and it just works.
23
- - 🌍 **Multilingual:** Understands English, Portuguese, Spanish, German,
24
- French, Chinese, Japanese and 100+ more, out of the box.
25
- - 💻 **Node.js, Bun and Browsers:** Native binary on Node and Bun, WebAssembly
26
- in the browser.
27
- - 📦 **Zero Dependencies:** `dependencies` is empty. Nothing to compile,
28
- nothing to install system-wide, nothing to keep patched.
29
- - 🧩 **Two Ways to Run It:** As a local HTTP service via the CLI, or in-process
30
- for zero network overhead.
31
-
32
- ---
33
-
34
- ## 📦 Installation
35
-
36
- ```bash
37
- # npm
38
- npm install laya-system-one
39
-
40
- # bun
41
- bun add laya-system-one
42
-
43
- # pnpm
44
- pnpm add laya-system-one
45
- ```
46
-
47
- The right engine for your machine is installed automatically. The model
48
- (~324 MB) is fetched once on first use and cached.
49
-
50
- ---
51
-
52
- ## 🚀 Quick Start
53
-
54
- ### 1. Launch the HTTP service
55
-
56
- ```bash
57
- npx laya-system-one --port 8080
58
- ```
59
-
60
- | Flag | Env | Default | Description |
61
- | :--- | :--- | :--- | :--- |
62
- | `--port <number>` | `PORT` | `8080` | Port to bind |
63
- | `--host <string>` | `HOST` | `0.0.0.0` | Address to bind |
64
- | `--backend <type>` | `LAYA_BACKEND` | `native` | `native` or `wasm` |
65
- | `--api-key <token>` | `LAYA_API_KEY` | *(none)* | Require Bearer auth on `/v1/systemone` |
66
-
67
- With authentication:
68
-
69
- ```bash
70
- npx laya-system-one --port 8080 --api-key secret-token-xyz
71
- ```
72
-
73
- ### 2. Use it in-process (zero network overhead)
74
-
75
- ```javascript
76
- import { Laya } from 'laya-system-one';
77
-
78
- // 1. Initialize the engine
79
- const laya = await Laya.load();
80
-
81
- // 2. Define the state (string, object, or array)
82
- const state = {
83
- customer_id: 'cust_9821',
84
- message: 'We were charged twice on our March invoice. Please refund the duplicate amount or we will cancel our plan.'
85
- };
86
-
87
- // 3. Define typed questions
88
- const questions = {
89
- department: {
90
- type: 'choice',
91
- instructions: 'Which team should resolve this customer inquiry?',
92
- criteria: {
93
- billing: 'Invoices, refunds, and duplicate charges',
94
- tech_support: 'Software bugs, outages, and error messages',
95
- sales: 'Upgrades, plan changes, and enterprise contracts'
96
- }
97
- },
98
- urgency: {
99
- type: 'score',
100
- instructions: 'Assess the urgency level of this inquiry.',
101
- criteria: ['Low / routine', 'Moderate', 'Critical / blocking / angry']
102
- },
103
- churn_risk: {
104
- type: 'noul',
105
- instructions: 'Does this message present an explicit risk of customer churn?',
106
- threshold: 0.5
107
- }
108
- };
109
-
110
- // 4. Evaluate
111
- const result = await laya.predict(state, questions);
112
-
113
- console.log(result.answers.department.choice); // -> "billing"
114
- console.log(result.answers.department.confidence); // -> 1.0
115
- console.log(result.answers.urgency.score); // -> 1.95
116
- console.log(result.answers.churn_risk.noul); // -> 0.968
117
- console.log(result.answers.churn_risk.decision); // -> true
118
- ```
119
-
120
- ### 3. Serve it from inside your app
121
-
122
- ```javascript
123
- import { serve } from 'laya-system-one';
124
-
125
- const srv = await serve({ host: '127.0.0.1', port: 8080, apiKey: 'optional-key' });
126
-
127
- console.log(`Laya server running at ${srv.url}/v1/systemone`);
128
-
129
- // later:
130
- await srv.close();
131
- ```
132
-
133
- ---
134
-
135
- ## 📡 HTTP API Reference (TypeSafe Jev compatible)
136
-
137
- ```http
138
- POST /v1/systemone
139
- Host: localhost:8080
140
- Content-Type: application/json
141
- Authorization: Bearer <API_KEY> [optional unless configured]
142
- ```
143
-
144
- | Parameter | Type | Required | Description |
145
- | :--- | :--- | :--- | :--- |
146
- | `state` | `string` \| `object` \| `array` | **Yes** | The context or text being evaluated. |
147
- | `questions` | `Record<string, Question>` | **Yes** | Map of question keys to typed questions. |
148
- | `model` | `string` | No | Model name (defaults to `laya-multilingual`, echoed back). |
149
-
150
- ### Question types
151
-
152
- **`choice`** — pick one of several options:
153
-
154
- ```json
155
- {
156
- "type": "choice",
157
- "instructions": "Which department should handle this ticket?",
158
- "criteria": {
159
- "billing": "Invoices and credit card transactions",
160
- "technical": "Software bugs and service disruptions"
161
- }
162
- }
163
- ```
164
-
165
- **`score`** — place on an ordered scale:
166
-
167
- ```json
168
- {
169
- "type": "score",
170
- "instructions": "Rate the severity of the issue.",
171
- "criteria": ["Minor cosmetic issue", "Degraded functionality", "Critical full service outage"]
172
- }
173
- ```
174
-
175
- **`noul`** — calibrated yes/no probability:
176
-
177
- ```json
178
- {
179
- "type": "noul",
180
- "instructions": "Does the user explicitly request a refund?",
181
- "threshold": 0.6
182
- }
183
- ```
184
-
185
- ### Example request
186
-
187
- ```bash
188
- curl -X POST http://localhost:8080/v1/systemone \
189
- -H "Content-Type: application/json" \
190
- -d '{
191
- "state": { "text": "Fui cobrado duas vezes na minha fatura. Reembolsem imediatamente." },
192
- "questions": {
193
- "dept": {
194
- "type": "choice",
195
- "instructions": "Which department should respond?",
196
- "criteria": { "billing": "Refunds, invoices, and payments", "support": "Technical and product questions" }
197
- },
198
- "urgency": {
199
- "type": "score",
200
- "instructions": "Urgency rating",
201
- "criteria": ["Low", "Medium", "High"]
202
- },
203
- "refund_demanded": {
204
- "type": "noul",
205
- "instructions": "Is the customer requesting a refund?",
206
- "threshold": 0.5
207
- }
208
- }
209
- }'
210
- ```
211
-
212
- ### Example response
213
-
214
- ```json
215
- {
216
- "model": "laya-multilingual",
217
- "answers": {
218
- "dept": {
219
- "type": "choice",
220
- "choice": "billing",
221
- "probabilities": { "billing": 1.0, "support": 0.0 },
222
- "confidence": 1.0
223
- },
224
- "urgency": {
225
- "type": "score",
226
- "score": 1.9482,
227
- "legend": { "0": "Low", "1": "Medium", "2": "High" },
228
- "probabilities": { "0": 0.0011, "1": 0.0496, "2": 0.9493 },
229
- "confidence": 0.9493
230
- },
231
- "refund_demanded": {
232
- "type": "noul",
233
- "noul": 0.9852,
234
- "confidence": 0.9852,
235
- "threshold": 0.5,
236
- "decision": true
237
- }
238
- },
239
- "usage": { "input_tokens": 82, "output_tokens": 12 }
240
- }
241
- ```
242
-
243
- ### Healthcheck
244
-
245
- ```http
246
- GET /health
247
- ```
248
-
249
- ```json
250
- {
251
- "status": "ok",
252
- "model": "laya-multilingual",
253
- "version": "1.1.0",
254
- "protocol": "TypeSafe Jev /v1/systemone compatible"
255
- }
256
- ```
257
-
258
- ### Error codes
259
-
260
- - `401 Unauthorized` — API key configured, header missing or wrong.
261
- - `422 Unprocessable Entity` — invalid JSON, or `state`/`questions` missing.
262
- - `404 Not Found` — unknown route.
263
- - `413 Payload Too Large` — body over 4 MB.
264
-
265
- ---
266
-
267
- ## ⚙️ Backends
268
-
269
- | Backend | How it runs | When to use |
270
- | :--- | :--- | :--- |
271
- | **`native`** *(default)* | A self-contained Rust server bundled with the package | The normal choice. Fastest, nothing to install. |
272
- | **`wasm`** | Pure Rust compiled to WebAssembly, also bundled | Browsers, or platforms with no native build. |
273
-
274
- Both ship inside the package — nothing is compiled or downloaded at install
275
- time. Switch with `--backend wasm` or `LAYA_BACKEND=wasm`.
276
-
277
- ---
278
-
279
- ## 📊 Performance
280
-
281
- Measured on real hardware, on every platform we ship a binary for, with the
282
- default `native` backend. Run `npm run bench` to measure your own machine.
283
-
284
- | Platform | Load | First answer | Warm (4 q/call) | Per question |
285
- | :--- | ---: | ---: | ---: | ---: |
286
- | macOS arm64 | 1.3 s | 268 ms | 220 ms | 51 ms |
287
- | Windows arm64 | 1.4 s | 231 ms | 178 ms | 47 ms |
288
- | Linux arm64 | 1.6 s | 184 ms | 145 ms | 40 ms |
289
- | Windows x64 | 1.7 s | 175 ms | 149 ms | 40 ms |
290
- | Linux arm64 (musl) | 1.9 s | 247 ms | 154 ms | 36 ms |
291
- | Linux x64 (musl) | 2.3 s | 387 ms | 253 ms | 59 ms |
292
- | Linux x64 | 2.5 s | 331 ms | 264 ms | 63 ms |
293
- | macOS x64 | 2.9 s | 387 ms | 360 ms | 84 ms |
294
-
295
- `Load` is reading the model into memory. `First answer` includes warmup. The
296
- warm numbers are sustained latency. Shared CI runners vary by ~20% between
297
- runs, so treat these as orders of magnitude rather than exact figures.
298
-
299
- The `wasm` backend is roughly 100x slower — it exists so browsers and unusual
300
- platforms work at all, not for throughput.
301
-
302
- ---
303
-
304
- ## 🧾 Environment variables
305
-
306
- | Variable | Effect |
307
- | :--- | :--- |
308
- | `LAYA_BACKEND` | `native` or `wasm` |
309
- | `LAYA_MODEL_PATH` | Use a `model.onnx` you already have (file or directory) |
310
- | `LAYA_MODEL_CHUNKS_DIR` | Directory holding the model chunks |
311
- | `LAYA_MODEL_URL` | Override where the model is downloaded from |
312
- | `LAYA_CACHE_DIR` | Where the model is cached |
313
- | `LAYA_PREFETCH_MODEL` | `1` = download the model during `npm install` |
314
- | `LAYA_SKIP_MODEL_DOWNLOAD` | `1` = never download, never prompt |
315
- | `LAYA_API_KEY` / `API_KEY` | Require `Authorization: Bearer <key>` |
316
- | `LAYA_SERVE_BIN` | Use a specific `laya-serve` binary |
317
-
318
- **Offline or air-gapped:**
319
-
320
- ```bash
321
- LAYA_PREFETCH_MODEL=1 npm install laya-system-one # fetch during install
322
- LAYA_MODEL_PATH=/opt/models/model.onnx # or bring your own copy
323
- LAYA_MODEL_CHUNKS_DIR=/opt/models/chunks # or a directory of chunks
324
- ```
325
-
326
- ---
327
-
328
- ## 💻 Requirements
329
-
330
- | | |
331
- | :--- | :--- |
332
- | **Node.js** | ≥ 18.17 |
333
- | **Bun** | ≥ 1.0 |
334
- | **Browsers** | The `wasm` backend |
335
- | **OS** | Linux (glibc and musl/Alpine), macOS (arm64 and x64), Windows (x64 and arm64) |
336
- | **Docker** | Debian, Ubuntu, Alpine |
337
-
338
- No runtime dependencies. The right native binary for your machine is installed
339
- automatically — nothing to compile, no system packages to add.
340
-
341
- ---
342
-
343
- ## 📥 What gets installed
344
-
345
- The package itself is small; the heavy parts arrive as dependencies npm picks
346
- for your platform, so you only download what you can run.
347
-
348
- | | Size |
349
- | :--- | ---: |
350
- | `laya-system-one` (code, tokenizer, wasm engine) | ~8.5 MB |
351
- | The one native binary for your platform | 8–26 MB |
352
- | The 13 model chunks | ~235 MB total |
353
- | The model on disk, after the first run | ~324 MB |
354
-
355
- The model is written next to the package when that directory is writable, and
356
- to your user cache otherwise — so `npm i -g` and read-only containers work
357
- without extra configuration. Every copy is checksum-verified, and a run that
358
- is killed mid-download leaves nothing corrupt behind.
359
-
360
- ---
361
-
362
- ## 🛠️ Development
363
-
364
- ```bash
365
- npm install
366
- npm run check # lint, unit, packaging, integration, e2e + install rehearsal
367
- npm run check:quick # the same minus the model-backed suites
368
- npm run test:rehearsal # install from a local registry and use it, Node + Bun
369
- npm run bench # measure on this machine
370
- npm run lint # syntax + packaging + docs consistency
371
- ```
372
-
373
- CI builds every platform, proves each binary answers 10 questions, and uploads
374
- the packages as artifacts. `verify-published.yml` installs a published version
375
- from the real registry on every platform — Node and Bun, including Alpine for
376
- musl — and runs a real inference.
377
-
378
- ---
379
-
380
- ## 📄 License & Attribution
381
-
382
- - **License:** [Apache-2.0](LICENSE)
383
- - **Author:** [Italo Almeida](https://github.com/italoalmeida0)
384
- - **Repository:** [https://github.com/italoalmeida0/laya-system-one](https://github.com/italoalmeida0/laya-system-one)
1
+ # Laya System-One ⚡
2
+
3
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+ [![Runtime](https://img.shields.io/badge/Runtime-Node.js%20%7C%20Bun%20%7C%20Browser-green.svg)]()
5
+ [![TypeSafe Jev](https://img.shields.io/badge/Wire%20Protocol-TypeSafe%20Jev%20Compatible-orange.svg)]()
6
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)]()
7
+ [![Built on Laya](https://img.shields.io/badge/Built%20on-Laya%20by%20Convai%20Innovations-8A2BE2.svg)](https://github.com/NandhaKishorM/laya)
8
+
9
+ > **A fast, self-contained decision engine. Give it any text and a set of typed questions, and it answers them — offline, in milliseconds, in over 100 languages. Drop-in compatible with the TypeSafe Jev API (`POST /v1/systemone`).**
10
+
11
+ Runs entirely on your machine. No Python, no PyTorch, no API keys, no cloud
12
+ calls at inference time. One `npm install` and it works.
13
+
14
+ > **Built on [Laya](https://github.com/NandhaKishorM/laya)** by
15
+ > [Convai Innovations](https://huggingface.co/convaiinnovations) — a
16
+ > community project. The model is theirs; this package makes it run in
17
+ > Node.js, Bun and the browser with no Python in the loop. See
18
+ > [Credits](#-credits).
19
+
20
+ ---
21
+
22
+ ## 🌟 Why Laya System-One?
23
+
24
+ - 🔒 **100% Offline:** Nothing leaves your machine. Ideal for corporate
25
+ intranets, edge servers and privacy-sensitive workflows.
26
+ - ⚡ **Fast:** ~40 ms per question on a warm engine, measured on every platform
27
+ we ship for.
28
+ - 🔄 **TypeSafe Jev Compatible:** Drop-in `POST /v1/systemone`. Point an
29
+ existing Jev client at it and it just works.
30
+ - 🌍 **Multilingual:** Understands English, Portuguese, Spanish, German,
31
+ French, Chinese, Japanese and 100+ more, out of the box.
32
+ - 💻 **Node.js, Bun and Browsers:** Native binary on Node and Bun, WebAssembly
33
+ in the browser.
34
+ - 📦 **Zero Dependencies:** `dependencies` is empty. Nothing to compile,
35
+ nothing to install system-wide, nothing to keep patched.
36
+ - 🧩 **Two Ways to Run It:** As a local HTTP service via the CLI, or in-process
37
+ for zero network overhead.
38
+
39
+ ---
40
+
41
+ ## 📦 Installation
42
+
43
+ ```bash
44
+ # npm
45
+ npm install laya-system-one
46
+
47
+ # bun
48
+ bun add laya-system-one
49
+
50
+ # pnpm
51
+ pnpm add laya-system-one
52
+ ```
53
+
54
+ The right engine for your machine is installed automatically. The model
55
+ (~324 MB) is fetched once on first use and cached.
56
+
57
+ ---
58
+
59
+ ## 🚀 Quick Start
60
+
61
+ ### 1. Launch the HTTP service
62
+
63
+ ```bash
64
+ npx laya-system-one --port 8080
65
+ ```
66
+
67
+ | Flag | Env | Default | Description |
68
+ | :--- | :--- | :--- | :--- |
69
+ | `--port <number>` | `PORT` | `8080` | Port to bind |
70
+ | `--host <string>` | `HOST` | `0.0.0.0` | Address to bind |
71
+ | `--backend <type>` | `LAYA_BACKEND` | `native` | `native` or `wasm` |
72
+ | `--api-key <token>` | `LAYA_API_KEY` | *(none)* | Require Bearer auth on `/v1/systemone` |
73
+
74
+ With authentication:
75
+
76
+ ```bash
77
+ npx laya-system-one --port 8080 --api-key secret-token-xyz
78
+ ```
79
+
80
+ ### 2. Use it in-process (zero network overhead)
81
+
82
+ ```javascript
83
+ import { Laya } from 'laya-system-one';
84
+
85
+ // 1. Initialize the engine
86
+ const laya = await Laya.load();
87
+
88
+ // 2. Define the state (string, object, or array)
89
+ const state = {
90
+ customer_id: 'cust_9821',
91
+ message: 'We were charged twice on our March invoice. Please refund the duplicate amount or we will cancel our plan.'
92
+ };
93
+
94
+ // 3. Define typed questions
95
+ const questions = {
96
+ department: {
97
+ type: 'choice',
98
+ instructions: 'Which team should resolve this customer inquiry?',
99
+ criteria: {
100
+ billing: 'Invoices, refunds, and duplicate charges',
101
+ tech_support: 'Software bugs, outages, and error messages',
102
+ sales: 'Upgrades, plan changes, and enterprise contracts'
103
+ }
104
+ },
105
+ urgency: {
106
+ type: 'score',
107
+ instructions: 'Assess the urgency level of this inquiry.',
108
+ criteria: ['Low / routine', 'Moderate', 'Critical / blocking / angry']
109
+ },
110
+ churn_risk: {
111
+ type: 'noul',
112
+ instructions: 'Does this message present an explicit risk of customer churn?',
113
+ threshold: 0.5
114
+ }
115
+ };
116
+
117
+ // 4. Evaluate
118
+ const result = await laya.predict(state, questions);
119
+
120
+ console.log(result.answers.department.choice); // -> "billing"
121
+ console.log(result.answers.department.confidence); // -> 1.0
122
+ console.log(result.answers.urgency.score); // -> 1.95
123
+ console.log(result.answers.churn_risk.noul); // -> 0.968
124
+ console.log(result.answers.churn_risk.decision); // -> true
125
+ ```
126
+
127
+ ### 3. Serve it from inside your app
128
+
129
+ ```javascript
130
+ import { serve } from 'laya-system-one';
131
+
132
+ const srv = await serve({ host: '127.0.0.1', port: 8080, apiKey: 'optional-key' });
133
+
134
+ console.log(`Laya server running at ${srv.url}/v1/systemone`);
135
+
136
+ // later:
137
+ await srv.close();
138
+ ```
139
+
140
+ ### `Laya.load(options)` options
141
+
142
+ | Option | Default | Description |
143
+ | :--- | :--- | :--- |
144
+ | `backend` | `'native'` | `native` (bundled Rust server) or `wasm` |
145
+ | `modelDir` | the package's `models/` | where `model.onnx` and `tokenizer.json` live |
146
+ | `maxLen` | `2048` | token budget for the state — raise it for long documents (max 8192, see [Long inputs](#long-inputs)) |
147
+ | `apiKey` | `null` | require a Bearer token on the HTTP layer |
148
+ | `port` / `host` | `0` / `127.0.0.1` | where the native server binds |
149
+ | `threads` | `0` | inference threads (`0` = runtime default) |
150
+
151
+ ---
152
+
153
+ ## 📡 HTTP API Reference (TypeSafe Jev compatible)
154
+
155
+ ```http
156
+ POST /v1/systemone
157
+ Host: localhost:8080
158
+ Content-Type: application/json
159
+ Authorization: Bearer <API_KEY> [optional unless configured]
160
+ ```
161
+
162
+ | Parameter | Type | Required | Description |
163
+ | :--- | :--- | :--- | :--- |
164
+ | `state` | `string` \| `object` \| `array` | **Yes** | The context or text being evaluated. |
165
+ | `questions` | `Record<string, Question>` | **Yes** | Map of question keys to typed questions. |
166
+ | `model` | `string` | No | Model name (defaults to `laya-multilingual`, echoed back). |
167
+
168
+ ### Question types
169
+
170
+ **`choice`** — pick one of several options:
171
+
172
+ ```json
173
+ {
174
+ "type": "choice",
175
+ "instructions": "Which department should handle this ticket?",
176
+ "criteria": {
177
+ "billing": "Invoices and credit card transactions",
178
+ "technical": "Software bugs and service disruptions"
179
+ }
180
+ }
181
+ ```
182
+
183
+ **`score`** — place on an ordered scale:
184
+
185
+ ```json
186
+ {
187
+ "type": "score",
188
+ "instructions": "Rate the severity of the issue.",
189
+ "criteria": ["Minor cosmetic issue", "Degraded functionality", "Critical full service outage"]
190
+ }
191
+ ```
192
+
193
+ **`noul`** — calibrated yes/no probability:
194
+
195
+ ```json
196
+ {
197
+ "type": "noul",
198
+ "instructions": "Does the user explicitly request a refund?",
199
+ "threshold": 0.6
200
+ }
201
+ ```
202
+
203
+ ### Example request
204
+
205
+ ```bash
206
+ curl -X POST http://localhost:8080/v1/systemone \
207
+ -H "Content-Type: application/json" \
208
+ -d '{
209
+ "state": { "text": "Fui cobrado duas vezes na minha fatura. Reembolsem imediatamente." },
210
+ "questions": {
211
+ "dept": {
212
+ "type": "choice",
213
+ "instructions": "Which department should respond?",
214
+ "criteria": { "billing": "Refunds, invoices, and payments", "support": "Technical and product questions" }
215
+ },
216
+ "urgency": {
217
+ "type": "score",
218
+ "instructions": "Urgency rating",
219
+ "criteria": ["Low", "Medium", "High"]
220
+ },
221
+ "refund_demanded": {
222
+ "type": "noul",
223
+ "instructions": "Is the customer requesting a refund?",
224
+ "threshold": 0.5
225
+ }
226
+ }
227
+ }'
228
+ ```
229
+
230
+ ### Example response
231
+
232
+ ```json
233
+ {
234
+ "model": "laya-multilingual",
235
+ "answers": {
236
+ "dept": {
237
+ "type": "choice",
238
+ "choice": "billing",
239
+ "probabilities": { "billing": 1.0, "support": 0.0 },
240
+ "confidence": 1.0
241
+ },
242
+ "urgency": {
243
+ "type": "score",
244
+ "score": 1.9482,
245
+ "legend": { "0": "Low", "1": "Medium", "2": "High" },
246
+ "probabilities": { "0": 0.0011, "1": 0.0496, "2": 0.9493 },
247
+ "confidence": 0.9493
248
+ },
249
+ "refund_demanded": {
250
+ "type": "noul",
251
+ "noul": 0.9852,
252
+ "confidence": 0.9852,
253
+ "threshold": 0.5,
254
+ "decision": true
255
+ }
256
+ },
257
+ "usage": { "input_tokens": 82, "output_tokens": 12 }
258
+ }
259
+ ```
260
+
261
+ ### Healthcheck
262
+
263
+ ```http
264
+ GET /health
265
+ ```
266
+
267
+ ```json
268
+ {
269
+ "status": "ok",
270
+ "model": "laya-multilingual",
271
+ "version": "1.1.0",
272
+ "protocol": "TypeSafe Jev /v1/systemone compatible"
273
+ }
274
+ ```
275
+
276
+ ### Error codes
277
+
278
+ - `401 Unauthorized` — API key configured, header missing or wrong.
279
+ - `422 Unprocessable Entity` — invalid JSON, or `state`/`questions` missing.
280
+ - `404 Not Found` — unknown route.
281
+ - `413 Payload Too Large` — body over 4 MB.
282
+
283
+ ---
284
+
285
+ ## ⚙️ Backends
286
+
287
+ | Backend | How it runs | When to use |
288
+ | :--- | :--- | :--- |
289
+ | **`native`** *(default)* | A self-contained Rust server bundled with the package | The normal choice. Fastest, nothing to install. |
290
+ | **`wasm`** | Pure Rust compiled to WebAssembly, also bundled | Browsers, or platforms with no native build. |
291
+
292
+ Both ship inside the package — nothing is compiled or downloaded at install
293
+ time. Switch with `--backend wasm` or `LAYA_BACKEND=wasm`.
294
+
295
+ ---
296
+
297
+ ## 📊 Performance
298
+
299
+ Measured on real hardware, on every platform we ship a binary for, with the
300
+ default `native` backend. Run `npm run bench` to measure your own machine.
301
+
302
+ | Platform | Load | First answer | Warm (4 q/call) | Per question |
303
+ | :--- | ---: | ---: | ---: | ---: |
304
+ | macOS arm64 | 1.3 s | 268 ms | 220 ms | 51 ms |
305
+ | Windows arm64 | 1.4 s | 231 ms | 178 ms | 47 ms |
306
+ | Linux arm64 | 1.6 s | 184 ms | 145 ms | 40 ms |
307
+ | Windows x64 | 1.7 s | 175 ms | 149 ms | 40 ms |
308
+ | Linux arm64 (musl) | 1.9 s | 247 ms | 154 ms | 36 ms |
309
+ | Linux x64 (musl) | 2.3 s | 387 ms | 253 ms | 59 ms |
310
+ | Linux x64 | 2.5 s | 331 ms | 264 ms | 63 ms |
311
+ | macOS x64 | 2.9 s | 387 ms | 360 ms | 84 ms |
312
+
313
+ `Load` is reading the model into memory. `First answer` includes warmup. The
314
+ warm numbers are sustained latency. Shared CI runners vary by ~20% between
315
+ runs, so treat these as orders of magnitude rather than exact figures.
316
+
317
+ The `wasm` backend is roughly 100x slower — it exists so browsers and unusual
318
+ platforms work at all, not for throughput.
319
+
320
+ ### Long inputs
321
+
322
+ The model reads up to **8,192 tokens**, but it ships with a conservative
323
+ **2,048-token** budget so it stays usable on weak machines. The budget is a
324
+ cap, not a cost: **short inputs are unaffected by raising it** — a 74-token
325
+ question answers in ~90 ms whatever the limit is, because the work follows the
326
+ input's real length.
327
+
328
+ Raise it when your inputs are long documents:
329
+
330
+ ```bash
331
+ LAYA_MAX_LEN=8192 npx laya-system-one --port 8080
332
+ ```
333
+
334
+ ```js
335
+ const laya = await Laya.load({ maxLen: 8192 });
336
+ ```
337
+
338
+ Measured on one machine (Windows arm64, `native` backend), by input length:
339
+
340
+ | tokens | default (2048) | `maxLen: 8192` |
341
+ | ---: | ---: | ---: |
342
+ | 74 | 88 ms | 88 ms |
343
+ | 1,000 | 0.9 s | 0.9 s |
344
+ | 2,000 | 6.4 s | 6.4 s |
345
+ | 4,000 | 9.2 s *(truncated)* | 21.3 s |
346
+ | 8,000 | 9.2 s *(truncated)* | 190 s |
347
+
348
+ Two things worth knowing before you raise it:
349
+
350
+ - **Accuracy degrades with length.** Upstream measured 16–18 of 20 requests
351
+ correct up to about 4,000 tokens, and 8–17 of 20 beyond that. Check your own
352
+ data — long-document accuracy is not something to assume.
353
+ - **Cost grows steeply.** Past ~2,000 tokens the time climbs faster than the
354
+ input does (attention is quadratic). 8,000 tokens is minutes, not seconds,
355
+ on a CPU. If you routinely handle documents that long, truncate them
356
+ yourself to the part that matters, or run the upstream Python package on a
357
+ GPU.
358
+
359
+ Truncation is the real risk of leaving it at the default: a long message gets
360
+ cut off and the answer can be wrong rather than slow. On a ~3,000-token input
361
+ the shipped default answered `sales` where the full text answers `billing`.
362
+
363
+ ---
364
+
365
+ ## 🧾 Environment variables
366
+
367
+ | Variable | Effect |
368
+ | :--- | :--- |
369
+ | `LAYA_BACKEND` | `native` or `wasm` |
370
+ | `LAYA_MAX_LEN` | token budget for the state (default 2048, max 8192) |
371
+ | `LAYA_MODEL_PATH` | Use a `model.onnx` you already have (file or directory) |
372
+ | `LAYA_MODEL_CHUNKS_DIR` | Directory holding the model chunks |
373
+ | `LAYA_MODEL_URL` | Override where the model is downloaded from |
374
+ | `LAYA_CACHE_DIR` | Where the model is cached |
375
+ | `LAYA_PREFETCH_MODEL` | `1` = download the model during `npm install` |
376
+ | `LAYA_SKIP_MODEL_DOWNLOAD` | `1` = never download, never prompt |
377
+ | `LAYA_API_KEY` / `API_KEY` | Require `Authorization: Bearer <key>` |
378
+ | `LAYA_SERVE_BIN` | Use a specific `laya-serve` binary |
379
+
380
+ **Offline or air-gapped:**
381
+
382
+ ```bash
383
+ LAYA_PREFETCH_MODEL=1 npm install laya-system-one # fetch during install
384
+ LAYA_MODEL_PATH=/opt/models/model.onnx # or bring your own copy
385
+ LAYA_MODEL_CHUNKS_DIR=/opt/models/chunks # or a directory of chunks
386
+ ```
387
+
388
+ ---
389
+
390
+ ## 💻 Requirements
391
+
392
+ | | |
393
+ | :--- | :--- |
394
+ | **Node.js** | ≥ 18.17 |
395
+ | **Bun** | ≥ 1.0 |
396
+ | **Browsers** | The `wasm` backend |
397
+ | **OS** | Linux (glibc and musl/Alpine), macOS (arm64 and x64), Windows (x64 and arm64) |
398
+ | **Docker** | Debian, Ubuntu, Alpine |
399
+
400
+ No runtime dependencies. The right native binary for your machine is installed
401
+ automatically — nothing to compile, no system packages to add.
402
+
403
+ ---
404
+
405
+ ## 📥 What gets installed
406
+
407
+ The package itself is small; the heavy parts arrive as dependencies npm picks
408
+ for your platform, so you only download what you can run.
409
+
410
+ | | Size |
411
+ | :--- | ---: |
412
+ | `laya-system-one` (code, tokenizer, wasm engine) | ~8.5 MB |
413
+ | The one native binary for your platform | 8–26 MB |
414
+ | The 13 model chunks | ~235 MB total |
415
+ | The model on disk, after the first run | ~324 MB |
416
+
417
+ The model is written next to the package when that directory is writable, and
418
+ to your user cache otherwise — so `npm i -g` and read-only containers work
419
+ without extra configuration. Every copy is checksum-verified, and a run that
420
+ is killed mid-download leaves nothing corrupt behind.
421
+
422
+ ---
423
+
424
+ ## 🛠️ Development
425
+
426
+ ```bash
427
+ npm install
428
+ npm run check # lint, unit, packaging, integration, e2e + install rehearsal
429
+ npm run check:quick # the same minus the model-backed suites
430
+ npm run test:rehearsal # install from a local registry and use it, Node + Bun
431
+ npm run bench # measure on this machine
432
+ npm run lint # syntax + packaging + docs consistency
433
+ ```
434
+
435
+ CI builds every platform, proves each binary answers 10 questions, and uploads
436
+ the packages as artifacts. `verify-published.yml` installs a published version
437
+ from the real registry on every platform — Node and Bun, including Alpine for
438
+ musl — and runs a real inference.
439
+
440
+ ---
441
+
442
+ ## 🙏 Credits
443
+
444
+ **This package would not exist without
445
+ [Laya](https://github.com/NandhaKishorM/laya).** It is a community project by
446
+ [Convai Innovations](https://huggingface.co/convaiinnovations) — the model,
447
+ the architecture, the training method and the wire protocol are all theirs.
448
+ What this package adds is a way to run it where Python is not an option:
449
+ Node.js, Bun and the browser.
450
+
451
+ | | |
452
+ | :--- | :--- |
453
+ | **Upstream project** | [NandhaKishorM/laya](https://github.com/NandhaKishorM/laya) |
454
+ | **Model** | [`convaiinnovations/laya-multilingual`](https://huggingface.co/convaiinnovations/laya-multilingual) (mmBERT-base, 322M params) |
455
+ | **Other checkpoints** | [`convaiinnovations/laya`](https://huggingface.co/convaiinnovations/laya) (English), [`laya-typed-decisions`](https://huggingface.co/convaiinnovations/laya-typed-decisions) |
456
+ | **Demo** | [Hugging Face Space](https://huggingface.co/spaces/convaiinnovations/laya-demo) |
457
+ | **Method** | RLCD — reinforcement learning against strictly proper scoring rules |
458
+ | **License** | Apache-2.0 (upstream and this package alike) |
459
+
460
+ If you find this useful, the credit belongs upstream — star
461
+ [their repository](https://github.com/NandhaKishorM/laya) and consider
462
+ [supporting the author](https://www.buymeacoffee.com/nandakishorm).
463
+
464
+ ## 📄 License
465
+
466
+ [Apache-2.0](LICENSE) — the same license as the upstream project.
467
+
468
+ - **This package:** [Italo Almeida](https://github.com/italoalmeida0) —
469
+ [laya-system-one](https://github.com/italoalmeida0/laya-system-one)
470
+ - **Model & upstream:** Convai Innovations —
471
+ [NandhaKishorM/laya](https://github.com/NandhaKishorM/laya)
@@ -103,5 +103,6 @@
103
103
  "npm": "https://registry.npmjs.org",
104
104
  "github": "https://github.com/italoalmeida0/laya-system-one/releases/download/v1.1.0/model.onnx",
105
105
  "githubLegacy": "https://github.com/italoalmeida0/laya-system-one/releases/download/v1.0.0/model.onnx"
106
- }
106
+ },
107
+ "modelVersion": "1.1.0"
107
108
  }
@@ -1,26 +1,26 @@
1
- {
2
- "encoder": "jhu-clsp/mmBERT-base",
3
- "head_layers": 2,
4
- "max_len": 1024,
5
- "head_max_len": 256,
6
- "max_prefixes": 6,
7
- "act_costs": {
8
- "escalate": 0.5
9
- },
10
- "cost_wrong_act": 3.0,
11
- "amp_dtype": "bf16",
12
- "model_name": "rl-agent",
13
- "temperature": [
14
- 1.0,
15
- 1.0,
16
- 1.0
17
- ],
18
- "temperature_by_options": {},
19
- "training": {
20
- "updates": 15987,
21
- "epochs_completed": 4,
22
- "hours": 4.97,
23
- "world_size": 1,
24
- "fine_tuned_from_checkpoint": false
25
- }
26
- }
1
+ {
2
+ "encoder": "jhu-clsp/mmBERT-base",
3
+ "head_layers": 2,
4
+ "max_len": 2048,
5
+ "head_max_len": 256,
6
+ "max_prefixes": 6,
7
+ "act_costs": {
8
+ "escalate": 0.5
9
+ },
10
+ "cost_wrong_act": 3,
11
+ "amp_dtype": "bf16",
12
+ "model_name": "rl-agent",
13
+ "temperature": [
14
+ 1,
15
+ 1,
16
+ 1
17
+ ],
18
+ "temperature_by_options": {},
19
+ "training": {
20
+ "updates": 15987,
21
+ "epochs_completed": 4,
22
+ "hours": 4.97,
23
+ "world_size": 1,
24
+ "fine_tuned_from_checkpoint": false
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laya-system-one",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Self-contained System 1 decision engine: multilingual INT8 transformer served by a zero-dependency native binary (or the bundled pure-Rust WASM engine). TypeSafe Jev wire-compatible (/v1/systemone). Runs offline in Node.js, Bun and browsers.",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -32,7 +32,7 @@
32
32
  "test:all": "npm run lint && npm run test:unit && npm run test:packaging && npm run test:integration && npm run test:e2e",
33
33
  "lint": "node ./tools/lint.js",
34
34
  "bundle": "node ./tools/make-bundle.js",
35
- "model:chunks": "node ./tools/model-chunks.js build --chunk-mb 24 --model-version $npm_package_version",
35
+ "model:chunks": "node ./tools/model-chunks.js build --chunk-mb 24",
36
36
  "model:assemble": "node ./tools/model-chunks.js assemble",
37
37
  "model:verify": "node ./tools/model-chunks.js verify",
38
38
  "model:publish": "node ./tools/model-chunks.js publish",
@@ -51,7 +51,9 @@
51
51
  "check": "node ./tools/local-check.js",
52
52
  "check:quick": "node ./tools/local-check.js --quick",
53
53
  "quick:check": "node ./tools/quick-check.js",
54
- "preflight": "node ./tools/preflight-publish.js"
54
+ "preflight": "node ./tools/preflight-publish.js",
55
+ "model:export": "python ./tools/export-model.py",
56
+ "model:diff": "node ./tools/model-diff.js"
55
57
  },
56
58
  "repository": {
57
59
  "type": "git",
@@ -79,13 +81,13 @@
79
81
  "homepage": "https://github.com/italoalmeida0/laya-system-one#readme",
80
82
  "dependencies": {},
81
83
  "optionalDependencies": {
82
- "@sys-one/laya-serve-darwin-arm64": "1.1.0",
83
- "@sys-one/laya-serve-darwin-x64": "1.1.0",
84
- "@sys-one/laya-serve-win32-x64": "1.1.0",
85
- "@sys-one/laya-serve-win32-arm64": "1.1.0",
86
- "@sys-one/laya-serve-linux-x64": "1.1.0",
87
- "@sys-one/laya-serve-linux-arm64": "1.1.0",
88
- "@sys-one/laya-serve-universal": "1.1.0",
84
+ "@sys-one/laya-serve-darwin-arm64": "1.2.0",
85
+ "@sys-one/laya-serve-darwin-x64": "1.2.0",
86
+ "@sys-one/laya-serve-win32-x64": "1.2.0",
87
+ "@sys-one/laya-serve-win32-arm64": "1.2.0",
88
+ "@sys-one/laya-serve-linux-x64": "1.2.0",
89
+ "@sys-one/laya-serve-linux-arm64": "1.2.0",
90
+ "@sys-one/laya-serve-universal": "1.2.0",
89
91
  "@sys-one/laya-model-chunk-00": "1.1.0",
90
92
  "@sys-one/laya-model-chunk-01": "1.1.0",
91
93
  "@sys-one/laya-model-chunk-02": "1.1.0",
package/src/agent.js CHANGED
@@ -165,6 +165,7 @@ export class Laya {
165
165
  port: options.port,
166
166
  apiKey: options.apiKey,
167
167
  threads: options.threads,
168
+ maxLen: options.maxLen,
168
169
  });
169
170
  await srv.start();
170
171
  return new LayaNative(srv, options);
@@ -307,6 +308,7 @@ export class LayaNative {
307
308
  port: options.port,
308
309
  apiKey: options.apiKey,
309
310
  threads: options.threads,
311
+ maxLen: options.maxLen,
310
312
  });
311
313
  await srv.start();
312
314
  return new LayaNative(srv, options);
@@ -136,6 +136,28 @@ function binaryCandidates() {
136
136
  return { exe, slots };
137
137
  }
138
138
 
139
+ /**
140
+ * The argument list for the native server. Pure, so the precedence between an
141
+ * explicit option, LAYA_MAX_LEN and the config file can be tested without
142
+ * spawning anything - a dropped option here silently leaves long inputs
143
+ * truncated with no error anywhere.
144
+ */
145
+ export function buildServerArgs({ modelDir, host, port, threads, apiKey, maxLen } = {}) {
146
+ const args = [
147
+ '--model-dir', modelDir,
148
+ '--host', host,
149
+ '--port', String(port),
150
+ '--threads', String(threads),
151
+ ];
152
+ // Token budget: an explicit option wins, then LAYA_MAX_LEN, then whatever
153
+ // the config file says (the binary applies that last fallback itself, so we
154
+ // only pass a value when the caller asked for one).
155
+ const budget = maxLen ?? (process.env.LAYA_MAX_LEN ? Number(process.env.LAYA_MAX_LEN) : null);
156
+ if (Number.isFinite(budget) && budget > 0) args.push('--max-len', String(budget));
157
+ if (apiKey) args.push('--api-key', apiKey);
158
+ return args;
159
+ }
160
+
139
161
  /**
140
162
  * Make sure a resolved binary is executable, and return it.
141
163
  *
@@ -209,6 +231,7 @@ export class NativeServer {
209
231
  this.port = options.port ?? 0;
210
232
  this.apiKey = options.apiKey || null;
211
233
  this.threads = options.threads ?? 0;
234
+ this.maxLen = options.maxLen ?? null;
212
235
  this.proc = null;
213
236
  this.url = null;
214
237
  }
@@ -232,12 +255,14 @@ export class NativeServer {
232
255
  ? bundledLib + ':' + spawnEnv.LD_LIBRARY_PATH
233
256
  : bundledLib;
234
257
  }
235
- const args = [
236
- '--model-dir', modelDir,
237
- '--host', this.host,
238
- '--port', String(this.port),
239
- '--threads', String(this.threads),
240
- ];
258
+ const args = buildServerArgs({
259
+ modelDir,
260
+ host: this.host,
261
+ port: this.port,
262
+ threads: this.threads,
263
+ apiKey: this.apiKey,
264
+ maxLen: this.maxLen
265
+ });
241
266
  if (this.apiKey) args.push('--api-key', this.apiKey);
242
267
  return new Promise((resolve, reject) => {
243
268
  const proc = spawn(bin, args, {