GitHub Hot Board

GitHub Hot Board · Repository archive

receptron/laya

Run Laya, the open-source Jev-compatible System-1 decision model, from Node.js / TypeScript via ONNX Runtime (This description comes from the repository itself.)

Data at October 22receptron

Best

#74

Dark Horses

985

Total stars

81

Total forks

1

Days on boards (30d)

README

@receptron/laya

Run Laya — the open-source, Jev-compatible System 1 decision model by Convai Innovations — from Node.js / TypeScript.

Laya does not generate text. You hand it a state (a ticket, an email, a JSON object) and typed questions, and it returns every answer with calibrated probabilities in one forward pass:

  • choice — pick one option, with a probability per option
  • score — an expected level on an ordered rubric, with the distribution
  • noul — a calibrated P(true) for a yes/no statement

This package runs the model with ONNX Runtime; PyTorch and Python are not needed at runtime. The request/response shape is the same as the Python reference implementation (RLAgent.system_one) and as TypeSafe Jev's system_one API, and the output matches the Python implementation to four decimal places.

Install

npm install @receptron/laya

Node.js 20 or newer; CI runs on Node 20, 22 and 24 (Linux, macOS, Windows). The @types/node version in devDependencies is only for type-checking this package and does not require Node 24.

The ONNX weights (about 1.7 GB, fp32) are downloaded from Hugging Face on first use and cached under ~/.cache/receptron-laya (override with LAYA_CACHE). Budget roughly 2 GB of RAM for the loaded model plus a few hundred MB per batch of questions.

The first Laya.load() therefore includes that download, so time it after the cache is warm. To fetch the weights ahead of time (in a build step or container image), call ensureBundle():

node -e 'import("@receptron/laya").then((m) => m.ensureBundle())'

Later loads only send a cheap freshness check and read from the cache (about 4 s on an Apple M4, measured in #13).

Usage

import { Laya } from "@receptron/laya";

const laya = await Laya.load();

const result = await laya.systemOne(
  { subject: "Refund not received", body: "I cancelled two weeks ago and still have no refund..." },
  {
    department: {
      type: "choice",
      instructions: "Which team should handle this ticket?",
      criteria: { billing: "payments, refunds, invoices", support: "product help and bugs", sales: "new purchases" },
    },
    urgency: {
      type: "score",
      instructions: "How urgent is this ticket?",
      criteria: ["not urgent", "somewhat urgent", "urgent", "critical"],
    },
    churn_risk: { type: "noul", instructions: "Is the customer likely to cancel or dispute?" },
  },
);

result.answers.department.choice; // "billing"
result.answers.department.probabilities; // { billing: 0.9415, support: 0.031, sales: 0.0275 }
result.answers.urgency.score; // 1.3886   (expected level, 0..3)
result.answers.churn_risk.noul; // 0.0988   (P(true))
result.answers.department.answer_confidence; // 0.9415   (max probability, on every answer type)
result.usage.input_tokens; // 267

await laya.close();

The answer types follow the question types, so result.answers.department is a ChoiceAnswer and result.answers.churn_risk a NoulAnswer without any casting.

To gate on confidence, use answer_confidence: the probability of the reported answer (max(p)), which is the quantity the checkpoint's temperatures are fitted for and means the same thing for every question type. confidence on choice / score answers is a different quantity (1 − normalized entropy).

Options

await Laya.load({
  modelDir: "./onnx", // use a local export instead of downloading (see below)
  repo: "receptron/laya-onnx", // Hugging Face repo that holds the ONNX bundle
  subfolder: "multilingual", // a checkpoint variant inside that repo
  revision: "main", // pin a commit hash for reproducible results; "main" follows the repo
  cacheDir: "/var/cache/laya",
  token: process.env.HF_TOKEN, // for private repos
  onProgress: ({ file, received, total }) => {}, // download progress
  executionProviders: ["cpu"], // onnxruntime-node execution providers
  sessionOptions: { intraOpNumThreads: 4 },
});

Every question of one systemOne call is batched into a single run; a call with three questions takes about 140 ms on an Apple-silicon CPU once the model is warm.

Exporting the ONNX bundle yourself

export/export_onnx.py turns the Hugging Face checkpoint (ModernBERT encoder + Laya's decision head) into one ONNX graph and copies the tokenizer and calibration values next to it. You only need this to build a bundle from a newer checkpoint or from a variant that is not published:

cd export
uv venv -p 3.12 .venv
uv pip install -p .venv/bin/python torch transformers safetensors onnx onnxscript onnxruntime huggingface_hub
.venv/bin/python -c "from huggingface_hub import snapshot_download; snapshot_download('convaiinnovations/laya', local_dir='model', allow_patterns=['model.safetensors','encoder/*','tokenizer/*','rl_agent_config.json','rl_common.py','rl_agent_api.py'])"
.venv/bin/python export_onnx.py model ../onnx   # prints the max logit difference vs. PyTorch (≈1e-5)

Then Laya.load({ modelDir: "./onnx" }). The bundle is the five files listed in BUNDLE_FILES: laya.onnx, laya.onnx.data, laya_config.json, tokenizer/tokenizer.json, tokenizer/tokenizer_config.json.

Limits

  • Each question's options must fit in head_max_len (192) tokens; systemOne throws otherwise. Fewer than about 20 options per choice question is the model's own recommendation.
  • The state is truncated to max_len (512 tokens for the English checkpoint) after the question header. A string or object state keeps its start; a list state (a conversation) keeps its newest end, as in training.
  • choice labels given as a list must be distinct; a repeated label throws instead of being merged.
  • Temperatures are clamped to [0.5, 5]. The published bundle uses 0.945 for choice questions with 11 or more options, where the upstream checkpoint ships 0.1006 (clamped to 0.5 by the Python laya package), so confidences for those questions differ from upstream by design.
  • A JSON state is serialized like Python's json.dumps(ensure_ascii=False) so that tokens match the reference implementation; non-integer numbers may format differently between JS and Python.

Development

yarn install
yarn test        # unit tests; the model test runs when ./onnx holds a bundle (or LAYA_MODEL_DIR)
yarn typecheck
yarn build
LAYA_MODEL_DIR=./onnx yarn example

License

MIT. The Laya model weights are published by Convai Innovations under Apache 2.0.

Current rankings

Momentum, past 30 days

Cumulative stars

Rank movement

← Back to boards