Crivo Web — Typed decisions

Run typed choice, noul and score decisions through the decide API, in Bun or the browser. A Commander.js CLI accepts the same state and decision definitions as JSON. Inference runs locally with WASM, WebGPU or native ONNX Runtime providers.

The runtime supports the Crivo family by tardellirs, retaining its ModernBERT encoder, question-type embedding and option scorer. No additional training was performed. Web compatibility and TypeScript integration by Yann Cabral (dizyann). Credit for the original architecture, weights and training belongs to the original authors. Model weights remain licensed CC BY-NC 4.0.

Try the interactive browser playground to edit requests and inspect model probabilities.

Installation and quick start

Install Bun 1.4+ and the Hugging Face CLI (hf), then download the source without the model weights. This includes nested source files, examples, configuration and the lockfile:

hf download dizyann/crivo-web --exclude 'onnx/*' --exclude 'models/*' --exclude 'tokenizer.json' --local-dir crivo-web
cd crivo-web
bun install --frozen-lockfile
bun run decide examples/basic/input.json --device cpu

The CLI announces missing model downloads before loading and prints a timing summary after the JSON response. The default 150M CPU mode downloads approximately 145 MiB of INT8 weights on first use. Later runs reuse the asset cache, but still initialize a model session.

bun run decide --help
cat examples/basic/input.json | bun run decide - --device cpu
bun run examples
bun run browser

The browser playground opens at http://localhost:4180/ and uses the same HTML, JSON editors, and persistent model worker as the published Space. Select CPU/WASM or GPU/WebGPU, model size and precision. GPU uses FP32 weights. The command builds the Space assets and serves them locally. Restart bun run browser after source changes; hot reloading is disabled.

Usage

The sole runtime export is decide. It accepts a JSON-serializable state and an array of independent decisions, returning answers keyed by decision ID. The tokenizer and resource lifecycle are implementation details; model execution can be configured through requirements. Type-only exports describe decisions, results, failures and the optional runtime provider.

import { decide } from './src/index.js'

const result = await decide({
  state: { message: 'Please refund the duplicate payment.' },
  decisions: [
    {
      id: 'intent',
      type: 'choice',
      instruction: 'Identify the primary customer intent.',
      options: [
        { value: 'refund', description: 'The customer wants money returned.' },
        { value: 'other', description: 'The customer has another request.' }
      ]
    },
    {
      id: 'duplicateCharge',
      type: 'noul',
      instruction: 'The customer reports a duplicate charge.',
      outcomes: {
        false: { description: 'There is no reported duplicate charge.' },
        true: { description: 'The same payment was charged more than once.' }
      }
    },
    {
      id: 'urgency',
      type: 'score',
      instruction: 'Rate the urgency using the ordered levels.',
      levels: [
        { value: 'low', description: 'An informational request with no loss.' },
        { value: 'high', description: 'A financial loss requiring prompt attention.' }
      ]
    }
  ]
})
  .onSuccess((answers) => ({ ok: true as const, answers }))
  .onError({ type: 'invalid-input' }, (error) => ({ ok: false as const, error }))
  .onError({ type: 'input-too-long' }, (error) => ({ ok: false as const, error }))

if (result.ok) {
  result.answers.intent.value // 'refund' | 'other'
  result.answers.intent.probabilities.refund // number
  result.answers.duplicateCharge.probability // P(true), between 0 and 1
  result.answers.urgency.score // expected zero-based level index, between 0 and 1 here
  result.answers.urgency.probabilities.high // number
}

decide.run(input, options) is equivalent to decide(input, options). Each run exposes id, status(), cancel(), onSuccess() and onError() controls. Cancellation rejects the run but does not interrupt an already-running inference; resource cleanup happens when the underlying work finishes.

Each decision requires a unique non-empty id and a non-empty instruction. Choice options and ordered score levels require unique non-empty value keys and non-empty description metadata. Optional label is presentation-only and is not sent to the model. At least two options/levels are required. Noul requires descriptions for both false and true. The decision array must not be empty.

The order of score levels is meaningful: score is the expected index from 0 to levels.length - 1, potentially fractional, not a percentage or a selected label. Probabilities use the supplied level values as keys. Each decision receives the same state, without consuming other decisions' answers.

Inline inputs infer literal IDs and option values automatically. For reusable input constants, use as const satisfies DecideInput. JSON loaded at runtime still needs a TypeScript type at the call site; runtime validation does not invent literal types. State supports JSON primitives, arrays and plain objects; cycles, undefined, bigint, non-finite numbers and class instances are rejected before model loading.

invalid-input contains path and message. input-too-long contains decisionId, tokens and limit. Handle declared failures before awaiting the run. Unexpected runtime failures (including download or backend errors) reject normally.

Select a model

Choose size (17m, 32m, 68m, 150m, 400m, or 1b), dtype (q8 or fp32), and device (auto, cpu, or gpu) through requirements.model, or the equivalent CLI flags. In the browser, WebGPU requires FP32; 1B on CPU/WASM requires INT8. The default size is 150M v2.

bun run decide examples/basic/input.json --size 68m --dtype q8 --device cpu
bun run decide examples/basic/input.json --size 400m --dtype fp32 --device gpu
const run = decide(input, {
  requirements: { model: { size: '68m', dtype: 'q8', device: 'cpu' } }
})

The default size is 150M v2, served from dizyann/crivo-web/models/150m. All variants live in models/<size>/ in this repository. The legacy root weights remain unchanged; Hugging Face redirects the former dizyann/crivo-150m-web URL. The only runtime export remains decide; ModelSize and ModelOptions are types. An injected decisionRuntime takes precedence over model options. When composing through a Layer, provide model: {} alongside a runtime provider if using defaults.

React: typed hook integration pattern

The following example describes an application-defined useCrivo hook. The project currently exports decide, not a React hook; this is a proposed integration contract, not a ready-to-import implementation. React and TanStack Query are not bundled.

The hook would use TanStack Query's useQuery for lazy model-session initialization, keyed by size, precision and device, with automatic refetching disabled. Its decide method would trigger inference explicitly, reuse the session and return typed answers. A browser implementation should run inference in a Worker, serialize requests sharing one session, and release resources when no longer used; query-cache eviction alone does not dispose ONNX sessions.

import { useState } from 'react'
import type { DecideInput } from './src/index.js'
import { useCrivo } from './hooks/useCrivo' // Application-defined; not provided yet.

const input = {
  state: { message: 'Please refund the duplicate payment.' },
  decisions: [
    {
      id: 'intent',
      type: 'choice',
      instruction: 'Identify the customer intent.',
      options: [
        { value: 'refund', description: 'The customer wants money returned.' },
        { value: 'other', description: 'Any other request.' }
      ]
    }
  ]
} as const satisfies DecideInput

function DecisionPanel() {
  const { decide, loading, deciding, error } = useCrivo({
    size: '150m',
    dtype: 'fp32',
    device: 'gpu'
  })

  const runDecision = () => decide(input)
  type Answers = Awaited<ReturnType<typeof runDecision>>
  const [answers, setAnswers] = useState<Answers | null>(null)

  async function handleDecide() {
    try {
      setAnswers(await runDecision())
    } catch {
      // The proposed hook exposes the failed request through `error`.
    }
  }

  // answers?.intent.value: 'refund' | 'other' | undefined
  // answers?.intent.probabilities.refund: number | undefined
  return (
    <section>
      <button disabled={loading || deciding} onClick={handleDecide}>
        {loading ? 'Loading model…' : deciding ? 'Deciding…' : 'Run'}
      </button>
      {error && <p role="alert">{error.message}</p>}
      {answers && <p>{answers.intent.value}</p>}
    </section>
  )
}

as const satisfies DecideInput validates the input while preserving literal IDs and option values. The hook's decide method must retain that generic relationship:

import type { DecideInput, Decision, DecisionResults } from './src/index.js'

type HookDecide = <const D extends readonly Decision[]>(
  input: DecideInput<D>
) => Promise<DecisionResults<D>>

This makes the component's state infer intent.value as 'refund' | 'other', without unknown, type assertions on answers, or a separately maintained response interface. The proposed hook also exposes preload() to initialize early, ready for session availability, and progress for download updates when available. loading covers model download/setup; deciding covers inference. Its promise rejects on invalid input or execution failure, and error exposes the failure for rendering.

Choosing a model

Latest checkpoints in the upstream collection, inspected October 19, 2026. These are author-reported Decision Index Public 0.3 scores, with all 140,178 requests completed. They are not accuracy percentages, Full scores, or benchmarks of our ONNX/INT8 conversions. Each model link pins the source card revision.

Model Public 0.3 ↑ Knowledge / Language / Retrieval / Tools / Arts ↑ Latency median / mean / p95 (ms) ↓ Published hardware
17M 22.15 1.2 / 21.0 / 40.1 / 36.5 / 17.0 7.7 / 9.7 / 12.4 A100 40 GB
32M v2 27.34 2.7 / 25.0 / 45.6 / 50.0 / 19.0 26.9 / 30.5 / 41.3 A100, shared workload
68M 32.82 5.0 / 30.8 / 48.8 / 64.0 / 20.9 16.4 / 21.2 / 28.6 A100
150M v2 34.38 8.3 / 34.5 / 48.2 / 62.8 / 21.8 18.0 / 26.1 / 44.4 A100
400M 39.79 11.5 / 44.3 / 51.0 / 69.0 / 25.4 23 / 38 / 74 A100
1B 47.77 18.5 / 56.3 / 55.8 / 78.7 / 28.9 13.2 / 36.4 / 94.1 H100 PCIe 80 GB

Category headings abbreviate Knowledge & Reasoning, Language, Retrieval & Classification, Tools & Automation, and Arts & Taste. Do not rank browser speed from this latency column: upstream uses BF16 on server GPUs; 32M v2 was measured alongside a training workload, and 1B uses a different GPU. Full benchmark scores are pending for these checkpoints. Older 32M/150M results belong to earlier weights.

Start with 17M or 32M when download size and memory dominate. Try 68M or 150M for more capable classification and automation. Evaluate 400M or 1B when the higher published scores justify larger downloads and memory use. These are starting points; measure your own task, especially confidence thresholds after INT8 quantization. The 68M Tools score exceeds 150M v2 here: bigger is not better on every category.

Select size, precision and device independently. CPU supports INT8 (q8) and FP32; WebGPU currently requires FP32: the tested INT8 graphs did not meet cross-backend numerical parity. INT8 stores weights in 8 bits and computes in FP32. It reduces download size; it does not guarantee faster inference or smaller runtime memory. FP32 is recommended for benchmark comparisons. Runtime memory exceeds weight download size, especially with long states or many questions. The browser wrapper caps each encoded question at 7,999 tokens; it does not claim upstream's extended context coverage.

Measured in the browser playground

Measured with bun run browser, Apple M4 / 24 GiB, macOS 26.6.2, headless Chromium 154, WebGPU via Metal, ONNX Runtime Web 1.30.0. The page was not cross-origin isolated (WASM threading unavailable). This was a local development session, without controlled power or thermal conditions. The default examples/basic request contains three questions in one batch. Each configuration had one initialization run, three more warmups, then 20 measured runs with the same session. Times include tokenization, ONNX inference and answer mapping; they exclude model setup and UI rendering. These are workload-specific measurements, not latency promises or a rerun of Decision Index. Longer inputs and different devices can change both latency and model rankings.

Warm inference: median / p95, milliseconds per complete three-question request.

Model CPU · WASM · INT8 CPU · WASM · FP32 GPU · WebGPU · FP32
17M 61.4 / 69.3 57.1 / 69.5 12.6 / 16.1
32M 187.0 / 215.2 168.0 / 172.0 14.6 / 16.4
68M 577.6 / 596.8 535.0 / 551.8 33.2 / 36.3
150M 1466.2 / 1526.9 1302.0 / 1368.1 68.4 / 70.1
400M 4406.5 / 4517.8 3903.4 / 4017.0 192.0 / 197.8
1B 12442.0 / 13207.4 Unsupported 486.8 / 504.3

Model setup from cached weights, seconds (one observation per configuration). This includes cached asset reads and session initialization, but not network downloads or the first inference. Reusing a session makes this setup time zero on subsequent runs.

Model CPU · WASM · INT8 CPU · WASM · FP32 GPU · WebGPU · FP32
17M 0.36 0.19 0.21
32M 0.22 0.26 0.30
68M 0.42 0.67 0.75
150M 0.62 1.33 1.58
400M 1.27 4.73 4.82
1B 3.72 — 6.31

INT8 reduces weight downloads; these measurements do not show a consistent speed advantage. For interactive browser use, evaluate smaller variants on WASM or FP32 on WebGPU, then validate decision quality on your own inputs. The 1B FP32/WASM combination is disabled because it exceeded the tested memory budget; INT8/WebGPU is also unsupported.

Raw samples and exact input · Reproduction procedure · UI benchmark script.

Download sizes and conversion checks

Weights only; tokenizer and runtime downloads are additional. Memory use is higher. The last column is the largest INT8–FP32 probability difference across the three conversion fixtures on native CPU, not an accuracy score.

Model FP32 (MiB) INT8 (MiB) Training context (tokens) INT8 max probability difference
17M 64.5 16.6 8,192 0.0270
32M 122.4 31.2 2,048 0.0260
68M 261.4 66.4 8,192 0.0321
150M 571.3 144.3 8,192 0.0863
400M 1510.8 380.1 8,192 0.0341
1B 3935.0 987.3 8,192 0.0088

FP32 was checked against PyTorch, native JavaScript ONNX Runtime, and WebGPU for all six variants. INT8 was checked on native CPU and WASM; WASM probabilities differed from the native INT8 fixtures by up to 0.034 in these checks. Backend-specific threshold calibration is necessary. The bundled numerical fixtures do not establish benchmark accuracy for INT8. 1B FP32 is disabled on WASM after an allocation failure in testing; choose INT8 on CPU or FP32 on WebGPU. Native CPU supports both. See scripts/conversion/results.json for provenance.

Execution

Importing the public entry does not load Transformers.js or download weights. The default runtime loads on demand, shares one session across the decisions of a run, and disposes it in finally. Separate calls create separate sessions; cached assets avoid repeated downloads, but session initialization still has a cost.

All questions in a request are encoded as rows of one ONNX batch and evaluated in one forward pass. Token and option padding are masked per question; answers keep the original IDs and option keys. FP32 batch probabilities match individual runs within the tested tolerance of 1e-4. INT8 changes probabilities. Compare equivalent batches and calibrate thresholds on the selected backend.

The default runtime selects INT8 on CPU/WASM and FP32 on GPU. Native assets use ./.cache/crivo; browser assets use Transformers.js Cache Storage. For the default 150M v2, downloads are approximately 145 MiB for INT8 or 572 MiB for FP32, plus runtime/tokenizer assets. Other sizes have different requirements. No hosted inference endpoint or API token is required for these public models.

A structural DecisionRuntime can be supplied through decide(input, { requirements: { decisionRuntime } }) for tests or another backend. A runtime must preserve IDs, answer discriminants and option keys, and owns cleanup of resources it allocates. Backend configuration lives in the composition, not in the state or decision definitions.

Development and examples

Terminal CLI

The Commander.js CLI accepts a JSON file containing { "state": ..., "decisions": [...] }, or - to read the same document from stdin. A complete request with all three decision types is available in examples/basic/input.json.

bun run decide --help
bun run decide examples/basic/input.json --device cpu
cat examples/basic/input.json | bun run decide - --device cpu
bun run decide request.json --device auto --cache-dir ./.cache/crivo

Options: --size 17m|32m|68m|150m|400m|1b (default 150m), --device auto|cpu|gpu (default auto), --dtype q8|fp32 (default selected for the device), and --cache-dir <path>. CLI configuration stays outside the request document. Success writes { "ok": true, "answers": ... } as JSON to stdout, with exit code 0. Input and inference failures write { "ok": false, "error": ... } to stderr, with exit code 1. Commander prints usage errors and help as text. Native backend diagnostics may also appear on stderr.

Before loading, the CLI checks the selected model's cache files and announces missing downloads with an estimate for the selected size and precision. Download progress, session initialization notices and the final timing summary go to stderr. The summary includes total elapsed time, loading/download time, inference time, resource cleanup time, model ID, precision and configured execution providers. Inference time includes input encoding and output decoding for all decisions. Providers describe the configured backend chain; individual graph nodes may fall back to CPU. Even with cached assets, session initialization takes time.

Development commands

bun install
bun run examples
bun run browser
bun run models:download --device cpu
bun run check
bun run typecheck
bun test
TEST_MODEL=1 bun test tests/model.integration.test.ts

examples/basic/input.ts demonstrates all three types and examples/basic/run.ts uses only the public API. After changing the typed input, run bun run examples:generate to update input.json; a test checks that both examples stay synchronized. The shared browser playground runs at http://localhost:4180/ with a Compute menu for CPU/WASM or GPU/WebGPU. The model integration test is opt-in; MODEL_CACHE selects an existing cache. Ordinary tests use a runtime provider and do not download or load model weights.

Implementation layout:

  • src/index.ts: public decide export and types.
  • src/decisions/: decision types, input validation and runtime requirement.
  • src/decisions/adapters/: internal Crivo/Transformers.js implementation and devices.
  • tests/: API behavior, compile-time inference and optional model integration.
  • examples/basic/: typed input and terminal example.
  • space/: shared HTML, JSON editors, and inference worker for local and hosted demos.
  • scripts/serve-space.ts: serves the built playground locally.
  • scripts/download-models.ts: downloads model assets into the local cache.
  • scripts/generate-example.ts: generates the CLI JSON example from its typed source.
  • Root model JSONs and onnx/: Hugging Face-compatible model assets.

Space demo

space/ contains the interactive Static Space. It runs inference in a Web Worker, with CPU/WASM selected by default and optional WebGPU. The Space keeps its worker and model session alive between runs, releasing the previous session when switching size, precision or backend on the next run. The CLI and default API runtime still release resources per call.

bun run space:build
hf upload dizyann/crivo-demo dist/space --type space

The build writes deployable assets and a Space-specific README to dist/space. All model variants live in dizyann/crivo-web, under models/<size>/, and download on demand.

Graph interface

Tensor Type Shape
input_ids int64 batch × sequence
attention_mask int64 batch × sequence
opt_positions int64 batch × options
opt_mask bool batch × options
qtype int64 batch
logits (output) float32 batch × options

Question types: noul=0, choice=1, score=2. The TypeScript wrapper implements all three, with a corresponding return type for each question.

Input format: [CLS] instructions [SEP] state [SEP] [OPT] key: description .... Serialize JSON state with sorted keys, compact separators, and unescaped Unicode. The example preserves the original encoding and pads sequences to multiples of 16. It rejects inputs above 7,999 tokens rather than truncating them.

Current API checks

  • Offline tests cover metadata validation, declared failures, runtime injection, CLI errors, download notices, timing summaries and example synchronization.
  • TypeScript checks cover inference of decision IDs, option values and answer types.
  • Opt-in CPU INT8 integration tests exercise all three decision types and verify session cleanup after a token-limit failure.
  • CLI file/stdin input and the default native backend were exercised with real weights.
  • The browser demo was exercised end to end with real CPU/WASM weights through bun run browser on Bun 1.4.0: choice, noul and score returned successfully with no JavaScript errors.

Historical model validation

These results describe the original conversion and examples. They are not accuracy claims for the current generic API; the circuit benchmark is no longer included.

  • FP32 ONNX was compared with the PyTorch implementation on three inputs with different sequence lengths and option counts; the export script checks logits using rtol=1e-3, atol=1e-4.
  • FP32 Transformers.js probabilities matched PyTorch on those three inputs within 1e-4 absolute error.
  • In a local Node.js CPU run, both FP32 and INT8 passed 27/27 circuit-editor examples with deterministic target-resolution rules. Median decision latency after one warmup was approximately 480 ms for FP32 and 196 ms for INT8. These are small, application-specific checks, not a general accuracy benchmark or a performance guarantee.
  • After renaming the circuit state key to prompt, the INT8 wrapper still passed all 27 circuit-editor examples. The three published examples were executed and their output types, probability ranges, and score calculation were checked before publishing.
  • Browser WASM and FP32 WebGPU ran choice/noul/score. In the three-question test, FP32 probability/score differences were below 1.4e-6. Browser on-demand loading from Hugging Face and cached loading with model-network requests blocked both passed.
  • Apple Silicon CoreML was tested through Bun; 610 of 1,561 graph nodes were assigned to CoreML, with the remainder on CPU. This does not establish a performance improvement over CPU.
  • INT8 changes numerical probabilities; validate confidence thresholds and accuracy on your own inputs.

Conversion details

The complete decision model was exported with PyTorch's legacy ONNX exporter, opset 17, using eager ModernBERT attention with reference compilation disabled. The current family uses per-channel signed INT8 weight storage with DequantizeLinear and FP32 arithmetic. This reduces downloads, but is not a promise of faster inference or lower runtime memory. The root legacy 150M files retain their original dynamic INT8 conversion. parity.json contains the original small PyTorch reference set. Inference and examples use TypeScript only.

Reproduce the family conversions

Conversion tools live under scripts/conversion/ and are not runtime dependencies. sources.json pins all six upstream snapshots; results.json records numerical checks. Use Python 3.14 and the pinned conversion packages:

python3 -m venv .cache/conversion-env
.cache/conversion-env/bin/pip install -r scripts/conversion/requirements.txt
.cache/conversion-env/bin/python scripts/conversion/download.py
.cache/conversion-env/bin/python scripts/conversion/export.py 68m
bun scripts/conversion/verify.ts 68m

Repeat the last two commands for each size. Output stays in .cache/variants/<size>; source weights stay local. Publish generated graphs and metadata into this repository’s models/<size>/ directories with python scripts/conversion/publish.py 17m 32m 68m 150m 400m 1b. The Hub stores the large binaries through Git LFS/Xet. Use GIT_LFS_SKIP_SMUDGE=1 git clone when cloning only to work on source code. The exporter checks varied sequence lengths, option counts and all three question types against the original PyTorch model. The JavaScript verifier checks the same fixtures against native ONNX Runtime. These are numerical smoke tests, not a rerun of Decision Index.

License and limitations

The original weights are licensed CC BY-NC 4.0, including the non-commercial restriction. This conversion retains that license. See the original model card for attribution, training sources, intended uses, and limitations. The original model is primarily English; historical Portuguese checks do not establish general multilingual accuracy. No endorsement by the original authors is implied.

Downloads last month
108
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for dizyann/crivo-web

Quantized
(4)
this model

Space using dizyann/crivo-web 1