a tiny pet with a GPT-2 brain, living in your browser
tokenling docs deployment.md
12 kB
Markdown
at main

Publishing #

The app is plain static files. There is no server component, database or account system. This page covers what to publish, what the web server must do, and how to ship and roll back a model. For the markup the app expects, see host-page.md.

Frontend build prerequisites #

Use Node.js 24 or newer and run npm ci from the repository before building. npm run typecheck checks all TS/TSX with strict mode. npm run build writes dist/; site/build.sh OUT remains the deployment/preview entry point. It checks types and bundles Preact plus the app into main.js, and inference into a separate brain.js, before replacing output. site/bundle.mjs accepts an output directory argument and reads no environment variables. The builder hashes emitted JavaScript and source maps together, so dependency changes also get a fresh immutable path. Model and runtime assets are still published separately. No Node process, package registry or Preact CDN is needed to serve or use the built app.

What gets published #

<base>/                      the page (index.html), sw.js and _headers, from site/build.sh
<base>/bench.html            unlinked brain benchmark using this release's model/runtime
<base>/assets/               the font, manifest.webmanifest and icons
<base>/app/<hash>/           bundled main.js, brain.js and source maps, under a content hash
<base>/models/<id>/          exported models (see export-and-runtime.md)
<base>/runtime/<name>/       the vendored transformers.js + ONNX Runtime files

<base> is the URL path the site is served under (/ for a whole domain).

Piece Produced by Notes
Page, benchmark, app, worker, assets BASE=<base> site/build.sh OUT fills the page and test/bench.html from site/release.json; leaves OUT/models and OUT/runtime alone
Model train/export.sh CKPT OUT writes tokenizer/config JSON, onnx/model_q4.onnx and SHA256SUMS (plus an eval/ file that is not published)
Runtime train/vendor.sh OUT pinned transformers.js and its matching ONNX Runtime build
Install model and runtime STATIC_ROOT=… train/publish.sh MODEL_DIR MODEL_ID RUNTIME_DIR RUNTIME_NAME verifies checksums first; BASE (default tokenling), FILE_MODE (default 0644) and DIR_MODE (default 0755) are optional

site/build.sh copies and renders the new static files in a temporary sibling directory before replacing output entries. A copy, release-validation or render failure during staging leaves the previous output untouched; models/ and runtime/ are preserved on successful builds too. The final multi-entry installation is not an atomic directory swap.

publish.sh finishes and checks the model partial and any needed runtime partial before publishing either. A new runtime is renamed first, then the model; a runtime-copy failure removes the partials without consuming the model id, so the same command can be retried. Existing runtime directories are left alone, and an existing model directory is never replaced. To build into the same tree, point site/build.sh at $STATIC_ROOT/$BASE.

site/release.json #

What the page says about the release. It is project data, not host data:

Key Meaning
modelId, size the current model directory and the byte size of its onnx/model_q4.onnx
trained one line for the page's details
news what the current brain does better, for the upgrade dialog
features features the current brain was trained for (host-page.md)
older older brains, newest first, {"id", "features"}, that visitors may keep running until they upgrade; only brains that speak the current prompt format
runtime the runtime directory name under <base>/runtime/
source URL of the source code for the page's link (SOURCE overrides it)

A deployment's external RELEASE file may also contain analyticsToken: the public Cloudflare Web Analytics site token. Keep deployment identifiers out of the repository's release file.

site/build.sh also takes MODELS_URL and RUNTIME_URL to load the model or runtime from other paths, and MODEL_ID/SIZE overrides (the preview uses them). CF_ANALYTICS_TOKEN overrides analyticsToken; an empty value disables analytics even when the release override contains a token. Both settings default to disabled and accept only an empty string or a 32-character hexadecimal token.

The build always includes bench.html, without a navigation link or analytics beacon. It uses the same resolved model/runtime URLs and model id as the app, including build overrides. It runs on opening, downloads its own model copy without using the pet's model cache, and reports results locally; it neither reads nor changes the saved pet. Serve it with isolation headers and Cache-Control: no-cache so a new release updates its model/runtime selection. See testing.md for its query options and measurements.

Optional page analytics #

With a configured token, the builder emits the Cloudflare Web Analytics module snippet before the closing body tag and adds a visible disclosure about page visits and performance. It never adds app event reporting or passes pet/owner fields, chat, diary or save data to analytics. Without a token, neither the snippet nor its disclosure is emitted.

Use the module form so the external script uses CORS while COOP/COEP remain enabled. After enabling it, verify that exactly one beacon script loads, its report succeeds, crossOriginIsolated remains true, and blocking the beacon or going offline does not stop the pet. Inspect a report from a synthetic test pet to confirm private app fields are absent. Provider setup, site tokens and deployment receipts belong in the operator's documentation. Avoid combining a manual snippet with a second automatically injected one.

Web server requirements #

Any static file server or static host works; nothing runs on the server.

  • HTTPS (or localhost). Cache Storage, persistent storage, service workers, notifications and threads need a secure context.
  • Trailing slash. Serve the page at <base>/ (redirect <base> to it): the service worker is registered relative to the page and its scope is that directory.
  • MIME types: .mjs and .js as text/javascript, .wasm as application/wasm, .json as JSON, .webmanifest as application/manifest+json. Range requests are not required.
  • Cross-origin isolation on the page and everything under <base>: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. Without them the WebAssembly backend runs on one thread: 2.0 tokens/s measured, against 5.9 with them (export-and-runtime.md). Cross-origin resources require appropriate CORS or CORP permission; keep the app, model and runtime same-origin.
  • Immutable caching for app/*, models/* and runtime/*: Cache-Control: public, max-age=31536000, immutable. This is only safe because those directories never change in place. Serve sw.js with Cache-Control: no-cache (or a short max-age) so worker updates arrive.
  • Compression is optional; the .onnx and .wasm files barely compress.
  • Allow for sizes: about 97 MB per model and about 85 MB of runtime (a browser fetches only one of the .wasm builds; the one transformers.js loads is 27 MB).

site/build.sh writes these header rules to <base>/_headers, the format some static hosts read. Other servers need the same rules in their configuration.

The shipped service worker passes .onnx downloads through to the browser without respondWith, including when MODELS_URL changes the path. Keep that bypass when embedding the app behind another service worker: proxying long model streams can end them early in Firefox (Error in input stream). The model's separate transformers-cache still supplies it offline.

Shipping a change #

Code or page only (nothing the model sees): run node test/parity.mjs and node test/contract.mjs, preview with test/dev-site.sh (see testing.md), build with site/build.sh and copy the output to the server. Keep old app/<hash>/ directories when copying (no delete-sync of app/): a tab opened before the deploy may still start its brain worker from its own version's directory.

A new model:

  1. train/export.sh CKPT OUT/tokenling-vN.
  2. train/publish.sh OUT/tokenling-vN tokenling-vN RUNTIME_DIR RUNTIME_NAME (the runtime step is skipped if that directory already exists).
  3. Edit site/release.json: the new modelId, size, trained and news; move the previous model to the front of older if the prompt format did not change (otherwise empty older); list any new features.
  4. Build and deploy the page, then test a fresh profile end to end (download, cached reload, a chat with each lookup tool, a diary page) and a profile that has the previous brain (the upgrade dialog, the download and the swap).

Visitors who already have a pet keep their brain until they accept the upgrade when its prompt format is still compatible; nobody loses a pet, chat or diary to a model change.

Rollback: point modelId back at the previous model (and take it out of older), rebuild and deploy. Old model directories can stay on disk. A visitor who already upgraded to the withdrawn brain has nothing usable cached and downloads the restored one again.

If the prompt format changed (see AGENTS.md), empty older: older brains cannot read the new format, so pets with only an old brain doze until the new one is downloaded. Saved pets still load, because their state stores plain chat lines, but expect a few odd replies until the old history scrolls out of the context. If a stored shape changed, bump the save version and add a migration (pet-simulation.md).

v6 changes sleeping replies and filters owner quotes from the chat header. Its release therefore has an empty older list; v2–v5 are incompatible.

Doing the GPU work #

Training and bulk teacher generation need the GPUs. On a shared machine:

  1. Look first. Check nvidia-smi and whatever else is serving from those GPUs for in-flight work, and note what is resident so you can restore it.
  2. Free the GPUs. Do not start jobs on cards other services need.
  3. train/teachers.sh start, wait, run gen_data.py, then stop. The script starts each server as its own process group and stops it by group, so nothing is killed by name pattern (a pkill -f pattern inside a shell command can match that same shell's command line).
  4. Train on one or both GPUs (CUDA_VISIBLE_DEVICES). Export and quantization are CPU-only and can run after the GPUs are given back.
  5. Hand the GPUs back as soon as they are free: restart whatever you stopped, restore what was resident, and verify each service with a real request. Do not hold the GPUs while only CPU work remains.
  6. Record the dates, the downtime and the results in your run receipt.

Live checks #

curl -fsSI https://HOST/<base>/                                  # 200, with the COOP/COEP headers
curl -fsS  https://HOST/<base>/models/tokenling-v6/SHA256SUMS
curl -fsSI https://HOST/<base>/sw.js                             # 200, text/javascript, not immutable
curl -fsSI https://HOST/<base>/assets/manifest.webmanifest       # 200, application/manifest+json
(cd <published model dir> && sha256sum -c SHA256SUMS)
curl -fsSI https://HOST/                                         # no COOP/COEP outside <base>, if you scoped them

Then run the browser flow in testing.md against the live URL with a fresh persistent profile, and check the layout at 360 px width.