Typed decisions on CPU, in milliseconds.

Sabaki sends a piece of text to the right tiny classifier and returns a typed answer, a choice, a score or a yes/no, with a calibrated confidence. No GPU, no API key, no per-call cost.

npm i sabaki

Read the guide npm GitHub

  • MIT license
  • No runtime dependencies in nano mode
  • Serves a Jev-compatible System One HTTP endpoint

Try it. It runs in your browser.

Shiba inu mascot peeking over the decision console

Router

  • customer_service 0.46
  • invoice_processing 1.24
  • security_incidents 0.00
  • agent_trace_observability 0.00

Picked @sabaki-js/invoice-processing with a score of 1.24 (threshold 0.15).

Typed answers

QuestionAnswerConfidence
duplicateprimary false 0.914automate
discrepancy_severity 2 0.575review
disposition hold 0.459review
matches_order false 0.673review
urgency 2 0.633review

Confidence 0.914 is above 0.8: safe to automate.

Routed and answered 5 questions at build time, with the same code.

Show the JSON
{
  "model": "@sabaki-js/invoice-processing@1.0.0+nano",
  "answers": {
    "duplicate": {
      "type": "choice",
      "choice": "false",
      "confidence": 0.914
    },
    "discrepancy_severity": {
      "type": "choice",
      "choice": "2",
      "confidence": 0.575
    },
    "disposition": {
      "type": "choice",
      "choice": "hold",
      "confidence": 0.459
    },
    "matches_order": {
      "type": "choice",
      "choice": "false",
      "confidence": 0.673
    },
    "urgency": {
      "type": "choice",
      "choice": "2",
      "confidence": 0.633
    }
  }
}

These are the real nano weights of the sabaki-js plugins, run by a port of the library’s own router and predictor. Nothing leaves this page. The plugins were trained on the benchmark’s structured English states, so free text earns modest confidence, which is exactly what the number is for.

Fast enough for a game loop

  • < 1 msper decision with nano, on a CPU
  • 12–18knano requests per second on one thread
  • ~20 msper decision with mini, int8 ONNX
  • 22 MBfor a mini model, down from 90 MB
  • 0GPUs, API keys or per-call fees
Shiba inu mascot sleeping peacefully on a CPU chip

Measured by the Sabaki authors on an Intel i5 and a Xeon, one thread. The benchmark script is in the plugin-doom repository.

A different way to ship AI decisions

A large language model is often used only to pick a label. Sabaki treats that as a classification problem and solves it with models of a few megabytes.

Shiba inu mascot shrugging, a question mark above its head
  1. Answers, not essays

    Sabaki never writes text. It answers typed questions: a choice among options, a score, or a yes/no probability. Each answer takes one forward pass, so there are no tokens to sample and nothing to parse.

  2. Plugins instead of one giant model

    A router scores the plugins by keyword overlap and sends the text to the best one. Each plugin is its own npm package, trained, versioned and shipped on its own. It is a mixture of experts at system level: a coarser gate than a jointly trained one, and much simpler to operate.

  3. Small enough to run anywhere

    Nano is a TF-IDF classifier of 20 to 650 KB with no dependencies. Mini is a fine-tuned MiniLM quantized to int8 ONNX, 22 MB. Both run on a regular CPU, inside your own process.

  4. It knows when it doesn’t know

    An unknown topic returns null, never a guess. Mini shows healthy doubt on out-of-distribution input (0.2 to 0.5 confidence on dragons, recipes and empty strings), which is what makes a rule like “automate above 0.8” safe.

How a decision is made

Sabaki follows the System One contract defined by TypeSafe, where models return typed answers and probabilities instead of generated text (System One docs).

Routing flow of a Sabaki decisionA state and its questions go to the router. A winning specialist plugin answers with nano or mini. When the domain is unknown, the generalist fallback answers. Both produce typed answers, served at POST /v1/systemone. State and questionsany text Routerhits / sqrt(keywords) Specialist pluginnano < 1 ms, mini ~20 ms Generalist fallbackreads runtime questions Typed answerschoice, score, noul POST /v1/systemone specialist wins unknown Routing flow of a Sabaki decisionA state and its questions go to the router. A winning specialist plugin answers with nano or mini. When the domain is unknown, the generalist fallback answers. Both produce typed answers, served at POST /v1/systemone. State and questionsany text Routerhits / sqrt(keywords) Specialist pluginnano < 1 ms, mini ~20 ms Generalist fallbackreads runtime questions specialist wins unknown Typed answerschoice, score, noul POST /v1/systemone
  1. Route

    The router lowercases the text, strips accents and counts keyword hits for each plugin. The score is hits divided by the square root of the keyword count. Below the plugin’s threshold (0.15 by default) the domain is unknown.

  2. Decide

    The winning plugin answers with nano (under 1 ms) or mini (about 20 ms). If no specialist wins, the optional generalist cross-encoder answers instead, reading questions you define at runtime.

  3. Answer

    You get typed answers with probabilities, in the Jev shape. The model field names the model that really answered. Over HTTP it is POST /v1/systemone.

Shiba inu mascot directing envelopes into three different tunnels

Three model sizes, one interface

Every plugin speaks the same interface, so you can start with nano and move a domain to mini when it needs more accuracy.

Tiny shiba inu puppy standing on a coin, a feather on its back

nano TF-IDF and logistic regression

Weights
20 to 650 KB
Latency
under 1 ms
Dependencies
none
Accuracy
0.34 to 0.90 per question

Pure TypeScript with exact scikit-learn parity, checked on 20 questions. It is not autoregressive: one pass, no tokens generated.

Shiba inu mascot wearing a small toolbelt and holding a magnifying glass

mini MiniLM-L6, fine-tuned per domain

Weights
22 MB, int8 ONNX
Latency
10 to 25 ms
Dependencies
onnxruntime-node, optional
Calibration
ECE below 0.16

One multi-task model per domain: a shared encoder with one linear head per question. Weights download once from the sabaki-models releases, are SHA256-verified and cached in ~/.cache/sabaki.

Shiba inu mascot reading an open question scroll

generalist Cross-encoder for open questions

Weights
22M parameters
Latency
20 to 36 ms
Dependencies
onnxruntime-node
Status
mini served (mix val 0.756, phishing R=0.616, 0.536 on typed-decisions test)

Answers anything: you pass instructions and up to 8 candidate options at runtime. It is the fallback when no specialist wins routing, and it never overrides one.

A larger base mode for hard tasks is under evaluation.

ONNX int8 quantization, measured

Dynamic int8 quantization shrinks a mini model from about 90 MB to 22 MB. On the customer-service benchmark it cost 0.004 of accuracy and left calibration unchanged or better. Every export is re-measured.

Fine-tuning beat the shortcuts

Fine-tuning the whole encoder scored 0.68 against 0.65 for a frozen encoder with MLP heads. Distilling a 400M teacher into frozen embeddings was tried and dropped.

How plugins are trained

A nano plugin

  1. Collect (text, label) pairs from public datasets or your own exports.
  2. Split deterministically: every fifth item is test, stratified across time. Training on the first items only inflated one score to 0.93, against 0.52 out of split.
  3. Fit TF-IDF (unigrams and bigrams, 3000 to 6000 features) and a logistic regression. Round coefficients to 1e-4.
  4. Export vocabulary, IDF and coefficients as JSON.
  5. Report accuracy and ECE on the held-out split in the plugin README.

A mini plugin

  1. Fine-tune MiniLM-L6 end to end for 8 to 12 epochs at 2e-5 on a CUDA GPU, one head per question.
  2. Export to ONNX and verify exact parity.
  3. Quantize to int8, then re-measure accuracy and ECE.

Privacy comes first: anonymize before training. On email, removing every personal term from the vocabulary cost 0.024 of accuracy.

Plugins: one npm package per domain

Each plugin ships its own nano weights and manifest. The four benchmark plugins also have a fine-tuned mini model, which shows how far a small specialist goes.

Shiba inu mascot plugging a puzzle-piece plug into a socket
Held-out accuracy of the nano model, with the mini score where one exists.
PackageDecidesQuestions and accuracyWeights
@sabaki-js/customer-service Support tickets
  • category 0.900 mini 0.91
  • needs_human 0.72
  • urgency 0.64
  • churn_risk 0.60
  • action 0.57
548 KB
@sabaki-js/invoice-processing Invoices, orders and deliveries
  • duplicate 0.89 mini 0.89
  • matches_order 0.66
  • urgency 0.63
  • discrepancy_severity 0.55
  • disposition 0.52
20 KB
@sabaki-js/security-incidents Security incident triage
  • true_positive 0.77
  • disposition 0.73 mini 0.76
  • credential_compromise 0.73
  • severity 0.63
  • urgency 0.57
476 KB
@sabaki-js/agent-trace-observability AI agent traces
  • needs_review 0.59 mini 0.82
  • risk 0.43
  • urgency 0.39
  • action 0.36
  • outcome 0.34
48 KB
@sabaki-js/mail Email triage in 8 buckets, plus phishing suspicion. The bucket score is 0.889 on test and 0.769 end to end; suspicion is experimental.
  • bucket 0.889
  • suspicious
655 KB
@sabaki-js/doom Doom navigation and combat decisions. Work in progress.
  • command
  • target
  • weapon
5 KB
@sabaki-js/plugin-template Starter for your own plugin.

Write a plugin in a few lines

A plugin is a manifest and a classify function. The threshold field sets the share of matched keywords below which the router says unknown.

  • definePlugin: any logic, no training.
  • nanoClassifier: bundled TF-IDF weights.
  • miniClassifier: a downloaded ONNX model.
  • autoClassifier: mini when it loads, nano otherwise.

Start from the plugin template

import { definePlugin } from "sabaki";

const ping = definePlugin({
  manifest: { name: "ping", domain: "ping", version: "0.1.0", questions: ["up"], keywords: ["ping"] },
  classify: () => ({ decision: "pong", confidence: 1, domain: "ping" }),
});

A 35 Hz game loop on a CPU

To prove the latency, Sabaki plays Doom. The game runs in Python (ViZDoom) and calls a JavaScript decision server once per tick. Mini serves about 71 requests per second on one thread and nano about 18,000, so a 35 Hz tick fits on any CPU. With an exit compass, mini reaches the exit of the first level in 2 to 4 of 6 runs; nano manages none, because text states lose spatial information. How it plays: each tick, sensors serialize the game into text plus an exit compass line; the model answers command, target, weapon over HTTP; a scripted executor (A* waypoints, door approach, 15-tick open-door latch) turns the command into Doom inputs. The model decides, the executor drives. It is a stress test, not a product.

Workflow technical details
  1. Sensors read engine internals every tick: labels buffer (per-object segmentation), sectors, positions.
  2. doomlib serializes the state to text plus one compass line (exit distance and bearing).
  3. The MiniLM model answers command, target and weapon over HTTP (/v1/systemone).
  4. The scripted executor (A* waypoints, door approach, 15-tick open-door latch) turns the command into the 14 game inputs.
  5. No vision model on this path; a separate 1.1M SFT head plays from pixels in arenas. No RL anywhere.

Watch the run · @sabaki-js/doom

Benchmarks, trade-offs included

On the typed-decisions benchmark, Sabaki gives up a few points of score for about 1000 times lower latency, measured against the 421M Laya model on a CPU.

Score on the typed-decisions test splits

  1. OpenDecider-nano 0.796 400M, GPU
  2. Laya typed-decisions 0.766 421M, CPU about 25 s
  3. TypeSafe Jev 0.754 API only
  4. sabaki mini 0.69 22M int8, about 20 ms
  5. sabaki nano 0.61 TF-IDF, under 1 ms
  6. Laya base 0.362 421M, zero-shot

CPU latency per decision, log scale

  1. sabaki nano < 1 ms
  2. sabaki mini about 20 ms
  3. sabaki generalist 20 to 36 ms
  4. Laya, 421M about 25 s per state

Scores for OpenDecider-nano, Laya and Jev are published numbers, not re-measured. The 25 s figure for Laya comes from the Sabaki authors’ CPU test of an ONNX port. The served generalist (mini, 22.7M) scores 0.536 on the typed-decisions test split (v4 scored 0.29); its 0.756 is its own mix validation — so it stays out of the chart above until the v1 numbers land.

Larger open decision models (144-151M), for reference

sabaki generalist is 22.7M - about 6.5x smaller than the smallest model listed here, and the smallest general-purpose decision model in the list. Figures are self-reported by each author; protocols differ.

Generalist generations (preliminary)
GenerationMix valPhishNChips recalltyped-decisions test
v40.848R=0.0260.29
v50.838R=0.066—
A (22M, 80k)0.898R=0.4060.546
mini v1.0.0 (served)0.756R=0.6160.536
B (144M, 80k, scratch)0.692R=0.0620.568
multi v8 (117M, non-latin fallback)0.554R=0.7360.31

Preliminary numbers while v6 trains: mix val is each run's own split (not comparable across rows); PhishNChips (2,000 held-out emails) and the typed-decisions test split are identical for every row. *n=200 subset.

Nano against mini, per workflow
WorkflowPrimary questionnanomini
customer_servicecategory0.9000.91
invoice_processingduplicate0.8900.89
security_incidentsdisposition0.7300.76
agent_traceneeds_review0.5900.82

Temporal split (generation batches kept apart): 300 train and 100 test states per workflow. On email, nano scores 0.865 with all personal terms removed, and MiniLM only ties TF-IDF (within 0.02), so nano ships.

How Sabaki compares

Against large typed-decision models, the trade is a few points of score for a model you can run anywhere.

SabakiLarge typed-decision models
Model sizeKilobytes of weights (nano), 22M int8 (mini)400M to 421M parameters (OpenDecider-nano, Laya)
Where it runsAny CPU, inside your processA GPU, or a hosted API (Jev)
CPU latencyUnder 1 ms (nano), about 20 ms (mini)About 25 s per state for Laya
Benchmark score0.61 (nano), 0.69 (mini)0.754 to 0.796
Cost per callNoneGPU time or API fees

A general LLM does far more, and you should use one when you need text, reasoning or open-ended answers. Sabaki is for the narrow case where you need a label in milliseconds, locally, with a confidence you can threshold.

Plugins
Domain experts are separate packages, so you install only the ones you route to.
ONNX int8 quantization
A mini model ships at about a quarter of its original size, for a cost of 0.004 accuracy.
Nano models
A classifier of kilobytes covers the cases where a bigger model adds little. On email, MiniLM tied TF-IDF, so nano ships.

Research paper

The design, measurements and trade-offs behind Sabaki, written up as a scientific paper.

Preprint v1 — October 2026

Sabaki: An Organizational Mixture-of-Experts for Typed Decisions on CPU

Fox3000foxy, Independent Researcher

Large language models are routinely invoked for tasks that are, at their core, classification. Sabaki replaces the single-model call with an organizational mixture-of-experts: a keyword- and language-aware router dispatches each input to a small, independently trained classifier plugin (nano TF-IDF, under 1 ms; mini MiniLM ONNX, about 20 ms on CPU; or a cross-encoder fallback) which returns a typed answer with calibrated confidence. On the public typed-decisions split it scores 0.61 (nano) and 0.69 (mini) against 0.75–0.80 for models needing a GPU or an API, for millisecond CPU latency instead of API round-trips (competitor latencies were never measured).

Read the PDF (v1)

Cite
@misc{sabaki2026orgmoe,
  title  = {Sabaki: An Organizational Mixture-of-Experts for Typed Decisions on CPU},
  author = {Fox3000foxy},
  year   = {2026},
  note   = {Preprint v1, October 2026},
  url    = {https://sabaki.fox3000foxy.com/assets/paper/sabaki-organizational-moe-typed-decisions.pdf}
}

Quickstart

Start with nano. It is synchronous, bundled and needs nothing else.

Nano

Register a plugin and call decide. Pass question to ask something other than the primary question.

npm i sabaki @sabaki-js/customer-service
import { Sabaki } from "sabaki";
import customerService from "@sabaki-js/customer-service";

const sabaki = new Sabaki().register(customerService);

const ticket =
  "Customer ticket: I was charged twice for my subscription, billing error on my account, please refund";

sabaki.decide({ state: ticket });
// { decision: "billing", confidence: 0.313, domain: "customer_service", plugin: "@sabaki-js/customer-service" }

sabaki.decide({ state: ticket, question: "needs_human" });
// { decision: "true", confidence: 0.783, ... }

Mini

Mini loads an ONNX model on first use and needs onnxruntime-node. Use the async path and a confidence threshold.

npm i sabaki @sabaki-js/customer-service onnxruntime-node
import { Sabaki, miniClassifier } from "sabaki";
import customerService from "@sabaki-js/customer-service";

// Downloads the int8 ONNX model once, SHA256-verified, cached in ~/.cache/sabaki
const mini = await miniClassifier(customerService.manifest, "customer_service");
const sabaki = new Sabaki().register(mini);

const r = await sabaki.decideAsync({ state: ticket, question: "category" });
if (r.confidence > 0.8) automate(r);
else escalate(r);

Generalist fallback

The generalist answers when no specialist wins routing. Point SABAKI_GENERALIST_DIR at a directory holding generalist.onnx and tokenizer/tokenizer.json. It is still training.

import { Sabaki, definePlugin, loadGeneralist } from "sabaki";
import mail from "@sabaki-js/mail";

const backend = loadGeneralist(process.env.SABAKI_GENERALIST_DIR);
const generalist = definePlugin({
  manifest: { name: "@sabaki-js/generalist", domain: "generalist", version: "0.1.0", questions: ["decision"], keywords: [] },
  classify: (input) => backend.decide(input),
});

const sabaki = new Sabaki().register(mail).setFallback(generalist);

// No specialist matches: the generalist answers your own question.
await sabaki.decideAsync({
  state: "The customer says the app deleted their data",
  question: "Is this urgent?",
  options: ["yes", "no"],
});

HTTP

A Jev-compatible server ships as an example in the repository. It is a development tool: add authentication before exposing it.

git clone https://github.com/sabaki-js/sabaki && cd sabaki
npm install && npm run build
node examples/serve.mjs 8000
curl -s localhost:8000/v1/systemone \
  -H 'content-type: application/json' \
  -d '{"state":"Customer ticket: I was charged twice, please refund","questions":{"category":{"type":"choice"}}}'
Result
{
  "model": "sabaki-mini",
  "answers": {
    "category": { "type": "choice", "choice": "<label>", "probabilities": { "<label>": 0.0 }, "confidence": 0.0 }
  },
  "usage": { "input_tokens": 0, "output_tokens": 0 }
}

Hosted API

No setup wanted? No panic! We host a free instance, always up to date, right here. Try it with the form below:

Or call it yourself:

curl
curl -s https://sabaki-pi.vercel.app/api/systemone -H 'content-type: application/json' -d '{"state":"I want a pizza","questions":{"q":{"type":"choice","instructions":"Pick one.","options":["order food","taxes"]}}}'

Need thousands of decisions, not one?

Bunkitsu resolves whole CSV files through the same router and plugins.

See how Bunkitsu works

npx bunkitsu resolve input.csv --out results.csv --plugin ./mon-plugin

What is not ready yet

The repository is candid about its gaps, and so is this page.

Read the full roadmap

  • The served generalist is mini (22.7M, from scratch on the full v6 mix): 0.756 on its own mix validation, phishing recall 0.616 on 2,000 held-out emails, 0.536 on the public typed-decisions test split.

  • A larger base mode for hard tasks is under evaluation.

  • Nano is weakest on agent traces (0.34 to 0.59). Mini lifts needs_review to 0.82.

  • Doom text states plateau around 0.57, so only the mini navigation run finishes the level.

  • The benchmark plugins are trained on English text, and the mail plugin on a mostly French mailbox. Multilingual training is planned if there is demand.

  • The HTTP server is a development tool. Add authentication before exposing it.

  • Shipped: per-question temperature scaling. Planned: a CLI and plugin hub (bunkitsu).

Frequently asked questions

What is Sabaki?

Sabaki is a TypeScript library that routes a piece of text to the right tiny classifier plugin and returns a typed answer (a choice, a score or a yes/no) with a calibrated confidence. It runs on a CPU, in milliseconds.

How is it different from a large language model?

It does not generate text. It answers typed questions in one forward pass, so you get a label and a confidence in about 1 ms (nano) or 20 ms (mini) with nothing to parse. Use an LLM when you need text or reasoning, and Sabaki when you need a decision.

Do I need a GPU or an API key?

No. Nano has no runtime dependencies. Mini runs through the optional onnxruntime-node package on a normal CPU. There is no per-call cost.

How accurate is it?

On the typed-decisions benchmark, mini scores 0.69 and nano 0.61, against 0.754 to 0.796 for models that need a GPU or an API. Confidence is calibrated (ECE below 0.16 for mini), so you can automate above a threshold such as 0.8 and escalate the rest.

What is the difference between nano, mini and the generalist?

Nano is a TF-IDF and logistic regression classifier of kilobytes. Mini is a fine-tuned MiniLM quantized to int8 ONNX, 22 MB. The generalist is a cross-encoder that answers questions you define at runtime, used as the fallback when no specialist matches.

Is it compatible with Jev and System One?

Sabaki serves a Jev-compatible subset of the System One contract over HTTP: POST /v1/systemone, GET /health and GET /v1/models. The model field names the model that actually answered.

Can I write my own plugin?

Yes. A plugin is a manifest and a classify function. Use definePlugin for any logic, nanoClassifier for TF-IDF weights, miniClassifier for an ONNX model, or start from the plugin template.

Which languages does it support?

The four benchmark plugins are trained on English text and the mail plugin on a mostly French mailbox. Multilingual training is on the roadmap if there is demand.

What is the license?

MIT. Packages are published to npm with provenance through trusted publishing, and everything runs locally, so your text stays with you.

Publisher: sabaki-js. Hosting: Cloudflare, Inc. This site collects no personal data: no accounts, no cookies, no analytics, no third-party scripts.