dizyann commited on
Commit
bd8c5ee
·
1 Parent(s): 4987a66

feat: consolidate model variants in crivo-web

Browse files
README.md CHANGED
@@ -1,6 +1,12 @@
1
  ---
2
  license: cc-by-nc-4.0
3
- base_model: tardellirs/crivo-150m
 
 
 
 
 
 
4
  library_name: transformers.js
5
  language:
6
  - en
@@ -74,8 +80,8 @@ const run = decide(input, {
74
  })
75
  ```
76
 
77
- The default size is 150M **v2**, served from `dizyann/crivo-150m-v2-web`.
78
- The original `dizyann/crivo-150m-web` weights remain unchanged for existing consumers.
79
  The only runtime export remains `decide`; `ModelSize` and `ModelOptions` are types.
80
  An injected `decisionRuntime` takes precedence over `model` options. When composing
81
  through a Layer, provide `model: {}` alongside a runtime provider if using defaults.
@@ -88,12 +94,12 @@ conversion fixtures on native CPU, not an accuracy score.
88
 
89
  | Model | FP32 (MiB) | INT8 (MiB) | Training context (tokens) | INT8 max probability difference |
90
  | --- | ---: | ---: | ---: | ---: |
91
- | [17M](https://huggingface.co/dizyann/crivo-17m-web) | 64.5 | 16.6 | 8,192 | 0.0270 |
92
- | [32M](https://huggingface.co/dizyann/crivo-32m-web) | 122.4 | 31.2 | 2,048 | 0.0260 |
93
- | [68M](https://huggingface.co/dizyann/crivo-68m-web) | 261.4 | 66.4 | 8,192 | 0.0321 |
94
- | [150M](https://huggingface.co/dizyann/crivo-150m-v2-web) | 571.3 | 144.3 | 8,192 | 0.0863 |
95
- | [400M](https://huggingface.co/dizyann/crivo-400m-web) | 1510.8 | 380.1 | 8,192 | 0.0341 |
96
- | [1B](https://huggingface.co/dizyann/crivo-1b-web) | 3935.0 | 987.3 | 8,192 | 0.0088 |
97
 
98
  FP32 was checked against PyTorch, native JavaScript ONNX Runtime, and WebGPU for
99
  all six variants. INT8 was checked on native CPU and WASM; WASM probabilities differed
@@ -108,7 +114,7 @@ Install Bun 1.4+ and the Hugging Face CLI (`hf`), then download the source witho
108
  model weights. This includes nested source files, examples, configuration and the lockfile:
109
 
110
  ```bash
111
- hf download dizyann/crivo-150m-web --exclude 'onnx/*' --exclude 'tokenizer.json' --local-dir crivo-web
112
  cd crivo-web
113
  bun install --frozen-lockfile
114
  bun run decide examples/basic/input.json --device cpu
@@ -373,7 +379,10 @@ bun scripts/conversion/verify.ts 68m
373
  ```
374
 
375
  Repeat the last two commands for each size. Output stays in `.cache/variants/<size>`;
376
- source weights and generated graphs are not committed to the application repository.
 
 
 
377
  The exporter checks varied sequence lengths, option counts and all three question types
378
  against the original PyTorch model. The JavaScript verifier checks the same fixtures
379
  against native ONNX Runtime. These are numerical smoke tests, not a rerun of Decision Index.
 
1
  ---
2
  license: cc-by-nc-4.0
3
+ base_model:
4
+ - tardellirs/crivo-17m
5
+ - tardellirs/crivo-32m
6
+ - tardellirs/crivo-68m
7
+ - tardellirs/crivo-150m
8
+ - tardellirs/crivo-400m
9
+ - tardellirs/crivo-1b
10
  library_name: transformers.js
11
  language:
12
  - en
 
80
  })
81
  ```
82
 
83
+ The default size is 150M **v2**, served from `dizyann/crivo-web/models/150m`.
84
+ 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.
85
  The only runtime export remains `decide`; `ModelSize` and `ModelOptions` are types.
86
  An injected `decisionRuntime` takes precedence over `model` options. When composing
87
  through a Layer, provide `model: {}` alongside a runtime provider if using defaults.
 
94
 
95
  | Model | FP32 (MiB) | INT8 (MiB) | Training context (tokens) | INT8 max probability difference |
96
  | --- | ---: | ---: | ---: | ---: |
97
+ | [17M](https://huggingface.co/dizyann/crivo-web/tree/main/models/17m) | 64.5 | 16.6 | 8,192 | 0.0270 |
98
+ | [32M](https://huggingface.co/dizyann/crivo-web/tree/main/models/32m) | 122.4 | 31.2 | 2,048 | 0.0260 |
99
+ | [68M](https://huggingface.co/dizyann/crivo-web/tree/main/models/68m) | 261.4 | 66.4 | 8,192 | 0.0321 |
100
+ | [150M](https://huggingface.co/dizyann/crivo-web/tree/main/models/150m) | 571.3 | 144.3 | 8,192 | 0.0863 |
101
+ | [400M](https://huggingface.co/dizyann/crivo-web/tree/main/models/400m) | 1510.8 | 380.1 | 8,192 | 0.0341 |
102
+ | [1B](https://huggingface.co/dizyann/crivo-web/tree/main/models/1b) | 3935.0 | 987.3 | 8,192 | 0.0088 |
103
 
104
  FP32 was checked against PyTorch, native JavaScript ONNX Runtime, and WebGPU for
105
  all six variants. INT8 was checked on native CPU and WASM; WASM probabilities differed
 
114
  model weights. This includes nested source files, examples, configuration and the lockfile:
115
 
116
  ```bash
117
+ hf download dizyann/crivo-web --exclude 'onnx/*' --exclude 'models/*' --exclude 'tokenizer.json' --local-dir crivo-web
118
  cd crivo-web
119
  bun install --frozen-lockfile
120
  bun run decide examples/basic/input.json --device cpu
 
379
  ```
380
 
381
  Repeat the last two commands for each size. Output stays in `.cache/variants/<size>`;
382
+ source weights stay local. Publish generated graphs and metadata into this repository’s
383
+ `models/<size>/` directories with `python scripts/conversion/publish.py 17m 32m 68m 150m 400m 1b`.
384
+ The Hub stores the large binaries through Git LFS/Xet. Use `GIT_LFS_SKIP_SMUDGE=1 git clone`
385
+ when cloning only to work on source code.
386
  The exporter checks varied sequence lengths, option counts and all three question types
387
  against the original PyTorch model. The JavaScript verifier checks the same fixtures
388
  against native ONNX Runtime. These are numerical smoke tests, not a rerun of Decision Index.
biome.jsonc CHANGED
@@ -2,7 +2,7 @@
2
  "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
3
  "files": {
4
  "ignoreUnknown": true,
5
- "includes": ["**", "!**/*.md", "!tokenizer.json", "!!**/.cache", "!!dist"]
6
  },
7
  "formatter": {
8
  "indentStyle": "space",
 
2
  "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
3
  "files": {
4
  "ignoreUnknown": true,
5
+ "includes": ["**", "!**/*.md", "!tokenizer.json", "!!**/.cache", "!!dist", "!!models"]
6
  },
7
  "formatter": {
8
  "indentStyle": "space",
scripts/benchmark/README.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Playground benchmark
2
+
3
+ Run `bun run browser`, open `http://localhost:4180` in a fresh browser tab, and paste
4
+ `playground.js` into DevTools before the first inference. Leave the default example
5
+ unchanged. The script operates the actual selectors and Run button and observes the
6
+ production worker messages; it does not substitute an inference implementation.
7
+
8
+ Inspect `window.crivoBenchmark.status` and `.error`. Once status is `complete`, save
9
+ `JSON.stringify(window.crivoBenchmark, null, 2)`. Each of the 17 supported configurations
10
+ runs once to initialize, three additional times to warm up, then 20 measured times.
11
+ All three questions are sent together in one request. Warm samples must report zero
12
+ model loading time. A 50 ms pause between requests lets the UI finish handling results.
13
+ Keep other workloads idle; hardware, power mode, browser and background activity affect timing.
14
+
15
+ `browser.json` records the measured input and all timing samples from the local playground.
16
+ Inference timing includes tokenization, the batched ONNX forward pass and answer mapping.
17
+ Worker total includes validation and other worker overhead, but excludes rendering and
18
+ main-thread messaging. First-run model setup and first inference are retained separately.
19
+ The README reports median and nearest-rank p95 inference times (19th of 20 sorted samples).
20
+
21
+ For the recorded run, exact locally converted weights were preloaded into the browser's
22
+ `transformers-cache` CacheStorage under their production Hugging Face resolve URLs.
23
+ These were the same artifacts published at the revision recorded in `browser.json`.
24
+ Model setup therefore measures cached asset reads and session initialization, **not downloads**.
25
+ To reproduce cached setup without local artifacts, run each selection once to download
26
+ its assets, reload the page, and then start the script. Do not clear browser storage.
27
+ Network speed is deliberately excluded. ONNX Runtime/CDN files may also be browser-cached.
28
+
29
+ The 1B FP32/WASM configuration is excluded because it exceeds the tested WASM memory
30
+ budget. INT8/WebGPU is unsupported. INT8 here is weight-only storage with FP32 arithmetic;
31
+ it is not expected to consistently outperform FP32.
scripts/benchmark/playground.js ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Run in a fresh `bun run browser` page's DevTools console. Uses the actual UI and worker.
2
+ window.crivoBenchmark = {
3
+ runs: [],
4
+ status: 'starting',
5
+ userAgent: navigator.userAgent,
6
+ threads: navigator.hardwareConcurrency
7
+ }
8
+ ;(async () => {
9
+ const report = window.crivoBenchmark
10
+ const adapter = await navigator.gpu?.requestAdapter()
11
+ report.gpu = adapter
12
+ ? {
13
+ ...Object.fromEntries(
14
+ ['vendor', 'architecture', 'device', 'description'].map((k) => [k, adapter.info[k]])
15
+ )
16
+ }
17
+ : null
18
+ const NativeWorker = window.Worker
19
+ let pending
20
+ let metrics
21
+ window.Worker = class extends NativeWorker {
22
+ constructor(...args) {
23
+ super(...args)
24
+ this.addEventListener('message', ({ data }) => {
25
+ if (data.kind === 'metrics') metrics = data.metrics
26
+ if (data.kind === 'error') pending?.reject(Error(data.message))
27
+ if (data.kind === 'result') {
28
+ if (!data.result.ok) pending?.reject(Error(JSON.stringify(data.result.error)))
29
+ else pending?.resolve({ ...metrics, totalMs: data.totalMs })
30
+ }
31
+ })
32
+ this.addEventListener('error', () => pending?.reject(Error('Worker stopped')))
33
+ }
34
+ postMessage(data, ...args) {
35
+ report.input = data.input
36
+ super.postMessage(data, ...args)
37
+ }
38
+ }
39
+ const run = () =>
40
+ new Promise((resolve, reject) => {
41
+ const timeout = setTimeout(() => reject(Error('Run timed out')), 240000)
42
+ pending = {
43
+ resolve: (data) => {
44
+ clearTimeout(timeout)
45
+ resolve(data)
46
+ },
47
+ reject: (error) => {
48
+ clearTimeout(timeout)
49
+ reject(error)
50
+ }
51
+ }
52
+ document.getElementById('run').click()
53
+ })
54
+ for (const size of ['17m', '32m', '68m', '150m', '400m', '1b']) {
55
+ for (const [device, dtype] of [
56
+ ['cpu', 'q8'],
57
+ ...(size === '1b' ? [] : [['cpu', 'fp32']]),
58
+ ['gpu', 'fp32']
59
+ ]) {
60
+ const entry = { size, device, dtype, samples: [] }
61
+ report.runs.push(entry)
62
+ report.status = `${size} ${device} ${dtype}`
63
+ for (const [id, value] of [
64
+ ['size', size],
65
+ ['device', device],
66
+ ['dtype', dtype]
67
+ ]) {
68
+ const select = document.getElementById(id)
69
+ select.value = value
70
+ select.dispatchEvent(new Event('change'))
71
+ }
72
+ await new Promise((resolve) => setTimeout(resolve, 50))
73
+ entry.first = await run()
74
+ for (let i = 0; i < 23; i++) {
75
+ await new Promise((resolve) => setTimeout(resolve, 50))
76
+ const result = await run()
77
+ if (result.loadMs !== 0) throw Error('Session was not reused')
78
+ if (i >= 3) entry.samples.push(result)
79
+ }
80
+ }
81
+ }
82
+ report.status = 'complete'
83
+ })().catch((error) => {
84
+ window.crivoBenchmark.error = String(error)
85
+ })
scripts/conversion/publish.py CHANGED
@@ -5,7 +5,7 @@ parser=argparse.ArgumentParser();parser.add_argument('sizes',nargs='+');args=par
5
  for size in args.sizes:
6
  directory=Path('.cache/variants')/size
7
  report=json.loads((directory/'parity.json').read_text())
8
- repo=f'dizyann/crivo-{size if size != "150m" else "150m-v2"}-web'
9
  source=report['source']
10
  card=f'''---
11
  license: cc-by-nc-4.0
@@ -20,7 +20,7 @@ Original architecture, training and weights by [tardellirs](https://huggingface.
20
  No additional training was performed. Weights retain the CC BY-NC 4.0 license.
21
 
22
  [Playground](https://huggingface.co/spaces/dizyann/crivo-demo) ·
23
- [TypeScript library, CLI and model comparison](https://huggingface.co/dizyann/crivo-150m-web)
24
 
25
  Source revision: `{source['revision']}`.
26
  See the [pinned original card](https://huggingface.co/{source['repo']}/blob/{source['revision']}/README.md)
@@ -41,4 +41,4 @@ older native ONNX Runtime versions can produce materially different INT8 probabi
41
  '''
42
  (directory/'README.md').write_text(card)
43
  subprocess.run(['hf','repos','create',repo,'--type','model','--exist-ok'],check=True)
44
- subprocess.run(['hf','upload',repo,str(directory),'--commit-message',f'Publish pinned Crivo {size} FP32 and INT8 web conversion'],check=True)
 
5
  for size in args.sizes:
6
  directory=Path('.cache/variants')/size
7
  report=json.loads((directory/'parity.json').read_text())
8
+ repo='dizyann/crivo-web'
9
  source=report['source']
10
  card=f'''---
11
  license: cc-by-nc-4.0
 
20
  No additional training was performed. Weights retain the CC BY-NC 4.0 license.
21
 
22
  [Playground](https://huggingface.co/spaces/dizyann/crivo-demo) ·
23
+ [TypeScript library, CLI and model comparison](https://huggingface.co/dizyann/crivo-web)
24
 
25
  Source revision: `{source['revision']}`.
26
  See the [pinned original card](https://huggingface.co/{source['repo']}/blob/{source['revision']}/README.md)
 
41
  '''
42
  (directory/'README.md').write_text(card)
43
  subprocess.run(['hf','repos','create',repo,'--type','model','--exist-ok'],check=True)
44
+ subprocess.run(['hf','upload',repo,str(directory),f'models/{size}','--commit-message',f'Publish pinned Crivo {size} FP32 and INT8 web conversion'],check=True)
space/README.md CHANGED
@@ -7,18 +7,13 @@ sdk: static
7
  app_file: index.html
8
  short_description: Typed decisions running locally in your browser
9
  models:
10
- - dizyann/crivo-17m-web
11
- - dizyann/crivo-32m-web
12
- - dizyann/crivo-68m-web
13
- - dizyann/crivo-150m-v2-web
14
- - dizyann/crivo-400m-web
15
- - dizyann/crivo-1b-web
16
  license: cc-by-nc-4.0
17
  ---
18
 
19
  # Crivo Decision Playground
20
 
21
- Explore choice, noul (probability of true), and ordered score decisions with [Crivo Web](https://huggingface.co/dizyann/crivo-150m-web).
22
 
23
  Edit the JSON state and decision definitions, then select **Run decisions**. Inference happens in your browser; request contents are not sent to a hosted inference service. Model and runtime assets download from the Hub and runtime CDN.
24
 
@@ -26,7 +21,7 @@ Choose 17M, 32M v2, 68M, 150M v2, 400M, or 1B. CPU/WASM supports INT8 weight sto
26
 
27
  Scores are expected zero-based level indices. Probabilities are model estimates, not guarantees. The original model is primarily English. Weights retain the original CC BY-NC 4.0 license and non-commercial restriction. Original model: [tardellirs/crivo-150m](https://huggingface.co/collections/tardellirs/crivo).
28
 
29
- Source and build instructions live in the [model repository](https://huggingface.co/dizyann/crivo-150m-web). Build with `bun run space:build`; upload `dist/space` to a Static Space.
30
 
31
  ## Credits
32
 
@@ -38,4 +33,4 @@ Questions follow the [official TypeSafe System One reference](https://docs.types
38
 
39
  The Space translates this format into the local Crivo API. Structured descriptions are serialized as JSON, null choice descriptions use the option name, and omitted noul descriptions use true/false statements. Crivo requires at least two choices and retains its own token limit. This is format compatibility, not identical model preprocessing or predictions. Responses use `model` and `answers`, with `choice`, `noul`, or `score`/`legend` fields. Confidence and token usage are omitted because Crivo does not expose them.
40
 
41
- All questions run together in a single ONNX batch. Dynamic INT8 quantization can make probabilities depend on the other questions in that batch; compare equivalent batches. FP32 batch/single equivalence is covered by the model integration tests.
 
7
  app_file: index.html
8
  short_description: Typed decisions running locally in your browser
9
  models:
10
+ - dizyann/crivo-web
 
 
 
 
 
11
  license: cc-by-nc-4.0
12
  ---
13
 
14
  # Crivo Decision Playground
15
 
16
+ Explore choice, noul (probability of true), and ordered score decisions with [Crivo Web](https://huggingface.co/dizyann/crivo-web).
17
 
18
  Edit the JSON state and decision definitions, then select **Run decisions**. Inference happens in your browser; request contents are not sent to a hosted inference service. Model and runtime assets download from the Hub and runtime CDN.
19
 
 
21
 
22
  Scores are expected zero-based level indices. Probabilities are model estimates, not guarantees. The original model is primarily English. Weights retain the original CC BY-NC 4.0 license and non-commercial restriction. Original model: [tardellirs/crivo-150m](https://huggingface.co/collections/tardellirs/crivo).
23
 
24
+ Source and build instructions live in the [model repository](https://huggingface.co/dizyann/crivo-web). Build with `bun run space:build`; upload `dist/space` to a Static Space.
25
 
26
  ## Credits
27
 
 
33
 
34
  The Space translates this format into the local Crivo API. Structured descriptions are serialized as JSON, null choice descriptions use the option name, and omitted noul descriptions use true/false statements. Crivo requires at least two choices and retains its own token limit. This is format compatibility, not identical model preprocessing or predictions. Responses use `model` and `answers`, with `choice`, `noul`, or `score`/`legend` fields. Confidence and token usage are omitted because Crivo does not expose them.
35
 
36
+ All questions run together in a single ONNX batch. Compare equivalent batches and precision settings when evaluating probabilities. FP32 batch/single equivalence is covered by the model integration tests.
space/app.ts CHANGED
@@ -114,7 +114,7 @@ function render(answers: Record<string, DecisionAnswer<Decision>>) {
114
  results.append(section)
115
  }
116
  outputEditor.value = JSON.stringify(
117
- toResponse(answers, runDecisions, modelFor(size.value as ModelSize).modelId),
118
  null,
119
  2
120
  )
@@ -196,7 +196,7 @@ element<HTMLFormElement>('form').onsubmit = (event) => {
196
  }
197
  render(data.result.answers)
198
  element('metrics').textContent =
199
- `${seconds(data.totalMs)} total · ${timings.loadMs === 0 ? 'session reused' : `${seconds(timings.loadMs)} model setup`} · ${seconds(timings.inferenceMs)} inference · ${dtype.value === 'q8' ? 'INT8' : 'FP32'} / ${device.value === 'gpu' ? 'WebGPU' : 'WASM'} · ${modelFor(size.value as ModelSize).modelId}`
200
  finish('Finished locally. Model stays ready for the next run.')
201
  }
202
  }
 
114
  results.append(section)
115
  }
116
  outputEditor.value = JSON.stringify(
117
+ toResponse(answers, runDecisions, modelFor(size.value as ModelSize).modelId, size.value),
118
  null,
119
  2
120
  )
 
196
  }
197
  render(data.result.answers)
198
  element('metrics').textContent =
199
+ `${seconds(data.totalMs)} total · ${timings.loadMs === 0 ? 'session reused' : `${seconds(timings.loadMs)} model setup`} · ${seconds(timings.inferenceMs)} inference · ${dtype.value === 'q8' ? 'INT8' : 'FP32'} / ${device.value === 'gpu' ? 'WebGPU' : 'WASM'} · ${modelFor(size.value as ModelSize).modelId} / ${size.value}`
200
  finish('Finished locally. Model stays ready for the next run.')
201
  }
202
  }
space/index.html CHANGED
@@ -2,7 +2,7 @@
2
  <html lang="en">
3
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Crivo — Decision playground</title><link rel="stylesheet" href="./style.css"><script type="module" src="./app.ts"></script></head>
4
  <body>
5
- <header><a class="brand" href="https://huggingface.co/dizyann/crivo-150m-web">crivo<span> / decision playground</span></a><a href="https://huggingface.co/dizyann/crivo-150m-web">Model & documentation ↗</a></header>
6
  <main>
7
  <section class="intro"><h1>Give context.<br>Get a decision.</h1><div><p>A small model for explicit choices.<br>Explore classification, yes/no probability, and ordered scores — right in your browser.</p><span class="local">Your input stays on this device.</span></div></section>
8
  <div class="workspace">
 
2
  <html lang="en">
3
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Crivo — Decision playground</title><link rel="stylesheet" href="./style.css"><script type="module" src="./app.ts"></script></head>
4
  <body>
5
+ <header><a class="brand" href="https://huggingface.co/dizyann/crivo-web">crivo<span> / decision playground</span></a><a href="https://huggingface.co/dizyann/crivo-web">Model & documentation ↗</a></header>
6
  <main>
7
  <section class="intro"><h1>Give context.<br>Get a decision.</h1><div><p>A small model for explicit choices.<br>Explore classification, yes/no probability, and ordered scores — right in your browser.</p><span class="local">Your input stays on this device.</span></div></section>
8
  <div class="workspace">
space/system-one.ts CHANGED
@@ -74,10 +74,12 @@ export function fromQuestions(state: unknown, questions: unknown): DecideInput {
74
  export function toResponse(
75
  answers: Record<string, DecisionAnswer<Decision>>,
76
  decisions: readonly Decision[],
77
- modelId = 'dizyann/crivo-150m-web'
 
78
  ) {
79
  return {
80
  model: modelId,
 
81
  answers: Object.fromEntries(
82
  Object.entries(answers).map(([id, answer]) => {
83
  if (answer.type === 'choice')
 
74
  export function toResponse(
75
  answers: Record<string, DecisionAnswer<Decision>>,
76
  decisions: readonly Decision[],
77
+ modelId = 'dizyann/crivo-web',
78
+ variant?: string
79
  ) {
80
  return {
81
  model: modelId,
82
+ ...(variant ? { variant } : {}),
83
  answers: Object.fromEntries(
84
  Object.entries(answers).map(([id, answer]) => {
85
  if (answer.type === 'choice')
space/worker.ts CHANGED
@@ -45,9 +45,7 @@ self.onmessage = async ({ data }) => {
45
  ? 'unknown'
46
  : cachedPaths.some(
47
  (path) =>
48
- path.includes(
49
- `/${data.size === '150m' ? 'crivo-150m-v2-web' : `crivo-${data.size}-web`}/`
50
- ) && path.endsWith(`/${progress.file}`)
51
  )
52
  ? 'cache'
53
  : 'network'
 
45
  ? 'unknown'
46
  : cachedPaths.some(
47
  (path) =>
48
+ path.includes('/dizyann/crivo-web/') && path.endsWith(`/${progress.file}`)
 
 
49
  )
50
  ? 'cache'
51
  : 'network'
src/cli/progress.ts CHANGED
@@ -34,7 +34,7 @@ export class TerminalProgress {
34
  )
35
  this.missing = new Set(files.filter((_, index) => !cached[index]))
36
  this.write(
37
- `Model: ${info.modelId} | precision: ${info.dtype} | providers: ${info.providers.join(', ')}`
38
  )
39
  if (this.missing.size) {
40
  const weightMiB =
@@ -81,7 +81,7 @@ export class TerminalProgress {
81
  )
82
  if (this.info)
83
  this.write(
84
- `Model: ${this.info.modelId} | precision: ${this.info.dtype} | providers: ${this.info.providers.join(', ')}`
85
  )
86
  }
87
  }
 
34
  )
35
  this.missing = new Set(files.filter((_, index) => !cached[index]))
36
  this.write(
37
+ `Model: ${info.modelId}${info.variant ? ` / ${info.variant}` : ''} | precision: ${info.dtype} | providers: ${info.providers.join(', ')}`
38
  )
39
  if (this.missing.size) {
40
  const weightMiB =
 
81
  )
82
  if (this.info)
83
  this.write(
84
+ `Model: ${this.info.modelId}${this.info.variant ? ` / ${this.info.variant}` : ''} | precision: ${this.info.dtype} | providers: ${this.info.providers.join(', ')}`
85
  )
86
  }
87
  }
src/decisions/adapters/crivo.ts CHANGED
@@ -10,6 +10,7 @@ export type { CrivoDevice } from './devices.js'
10
 
11
  export interface ModelLoadInfo {
12
  modelId: string
 
13
  dtype: 'q8' | 'fp32'
14
  providers: string[]
15
  weightMiB?: number
@@ -99,11 +100,13 @@ export class Crivo {
99
  static async load(options: CrivoLoadOptions = {}): Promise<Crivo> {
100
  const selected = modelFor(options.size)
101
  const modelId = options.modelId ?? selected.modelId
 
102
  const execution = await resolveExecution(options)
103
  const native = typeof process !== 'undefined' && process.release?.name === 'node'
104
  if (
105
  !native &&
106
- modelId === modelFor('1b').modelId &&
 
107
  execution.device === 'wasm' &&
108
  execution.dtype === 'fp32'
109
  )
@@ -120,10 +123,13 @@ export class Crivo {
120
  const cacheDir = options.cacheDir ?? (native ? './.cache/crivo' : undefined)
121
  await options.onLoadStart?.({
122
  modelId,
123
- ...(!options.modelId
124
  ? {
125
  weightMiB: Math.ceil(selected.assets[execution.dtype].bytes / 1024 ** 2),
126
- weightFiles: selected.assets[execution.dtype].files
 
 
 
127
  }
128
  : {}),
129
  dtype: execution.dtype,
@@ -140,7 +146,13 @@ export class Crivo {
140
  const tokenizer = await AutoTokenizer.from_pretrained(modelId, loading)
141
  const model = await PreTrainedModel.from_pretrained(modelId, {
142
  ...loading,
143
- ...execution
 
 
 
 
 
 
144
  })
145
  // An encoder-only export cannot reproduce the Crivo decision interface.
146
  const session = model.sessions.model as {
 
10
 
11
  export interface ModelLoadInfo {
12
  modelId: string
13
+ variant?: ModelSize
14
  dtype: 'q8' | 'fp32'
15
  providers: string[]
16
  weightMiB?: number
 
100
  static async load(options: CrivoLoadOptions = {}): Promise<Crivo> {
101
  const selected = modelFor(options.size)
102
  const modelId = options.modelId ?? selected.modelId
103
+ const family = !options.modelId || options.modelId === selected.modelId
104
  const execution = await resolveExecution(options)
105
  const native = typeof process !== 'undefined' && process.release?.name === 'node'
106
  if (
107
  !native &&
108
+ family &&
109
+ selected.size === '1b' &&
110
  execution.device === 'wasm' &&
111
  execution.dtype === 'fp32'
112
  )
 
123
  const cacheDir = options.cacheDir ?? (native ? './.cache/crivo' : undefined)
124
  await options.onLoadStart?.({
125
  modelId,
126
+ ...(family
127
  ? {
128
  weightMiB: Math.ceil(selected.assets[execution.dtype].bytes / 1024 ** 2),
129
+ variant: selected.size,
130
+ weightFiles: selected.assets[execution.dtype].files.map(
131
+ (file) => `models/${selected.size}/${file}`
132
+ )
133
  }
134
  : {}),
135
  dtype: execution.dtype,
 
146
  const tokenizer = await AutoTokenizer.from_pretrained(modelId, loading)
147
  const model = await PreTrainedModel.from_pretrained(modelId, {
148
  ...loading,
149
+ ...execution,
150
+ ...(family
151
+ ? {
152
+ subfolder: selected.subfolder,
153
+ use_external_data_format: selected.assets[execution.dtype].files.length - 1
154
+ }
155
+ : {})
156
  })
157
  // An encoder-only export cannot reproduce the Crivo decision interface.
158
  const session = model.sessions.model as {
src/decisions/models.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { MODEL_ASSETS } from './model-assets.js'
2
- /** Latest upstream checkpoints. The legacy 150M repository remains addressable by modelId. */
3
  export const MODEL_SIZES = ['17m', '32m', '68m', '150m', '400m', '1b'] as const
4
  export type ModelSize = (typeof MODEL_SIZES)[number]
5
  export interface ModelOptions {
@@ -7,16 +7,9 @@ export interface ModelOptions {
7
  dtype?: 'q8' | 'fp32'
8
  device?: 'cpu' | 'gpu' | 'auto'
9
  }
10
- const MODELS = {
11
- '17m': { modelId: 'dizyann/crivo-17m-web' },
12
- '32m': { modelId: 'dizyann/crivo-32m-web' },
13
- '68m': { modelId: 'dizyann/crivo-68m-web' },
14
- '150m': { modelId: 'dizyann/crivo-150m-v2-web' },
15
- '400m': { modelId: 'dizyann/crivo-400m-web' },
16
- '1b': { modelId: 'dizyann/crivo-1b-web' }
17
- } satisfies Record<ModelSize, { modelId: string }>
18
 
19
  export function modelFor(size: ModelSize = '150m') {
20
- if (!Object.hasOwn(MODELS, size)) throw new Error(`Unknown Crivo size: ${size}`)
21
- return { ...MODELS[size], assets: MODEL_ASSETS[size] }
22
  }
 
1
  import { MODEL_ASSETS } from './model-assets.js'
2
+ /** Latest upstream checkpoints. All sizes share one Hub repository and tokenizer. */
3
  export const MODEL_SIZES = ['17m', '32m', '68m', '150m', '400m', '1b'] as const
4
  export type ModelSize = (typeof MODEL_SIZES)[number]
5
  export interface ModelOptions {
 
7
  dtype?: 'q8' | 'fp32'
8
  device?: 'cpu' | 'gpu' | 'auto'
9
  }
10
+ export const MODEL_REPO = 'dizyann/crivo-web'
 
 
 
 
 
 
 
11
 
12
  export function modelFor(size: ModelSize = '150m') {
13
+ if (!Object.hasOwn(MODEL_ASSETS, size)) throw new Error(`Unknown Crivo size: ${size}`)
14
+ return { modelId: MODEL_REPO, size, subfolder: `models/${size}/onnx`, assets: MODEL_ASSETS[size] }
15
  }
tests/models.test.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect, test } from 'bun:test'
2
+ import { MODEL_REPO, MODEL_SIZES, modelFor } from '../src/decisions/models.js'
3
+
4
+ test('every variant resolves to its own graph folder in the shared repository', () => {
5
+ expect(MODEL_REPO).toBe('dizyann/crivo-web')
6
+ expect(modelFor().size).toBe('150m')
7
+ const folders = MODEL_SIZES.map((size) => {
8
+ const model = modelFor(size)
9
+ expect(model.modelId).toBe(MODEL_REPO)
10
+ expect(model.subfolder).toBe(`models/${size}/onnx`)
11
+ return model.subfolder
12
+ })
13
+ expect(new Set(folders).size).toBe(6)
14
+ expect(modelFor('1b').assets.fp32.files).toHaveLength(5)
15
+ expect(modelFor('1b').assets.q8.files).toHaveLength(2)
16
+ })
tests/system-one.test.ts CHANGED
@@ -37,7 +37,7 @@ test('System One questions map to all three local decision types', () => {
37
  request.decisions
38
  )
39
  ).toEqual({
40
- model: 'dizyann/crivo-150m-web',
41
  answers: {
42
  route: { type: 'choice', choice: 'billing', probabilities: { billing: 0.8, support: 0.2 } },
43
  urgent: { type: 'noul', noul: 0.9 },
 
37
  request.decisions
38
  )
39
  ).toEqual({
40
+ model: 'dizyann/crivo-web',
41
  answers: {
42
  route: { type: 'choice', choice: 'billing', probabilities: { billing: 0.8, support: 0.2 } },
43
  urgent: { type: 'noul', noul: 0.9 },