npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@johnhenry/math-plus-safetensors

v0.1.3

Published

Universal safetensors reader/writer: validated headers, typed views (F16 as Float16Array, BF16 bits), lazy reads from Blob, HTTP ranges and file handles, tensor-core interop — part of Math Plus

Downloads

1,017

Readme

@johnhenry/math-plus-safetensors

npm version license

Read and write safetensors files in any JS runtime — Node, Bun, Deno, browsers. Headers are validated like the reference implementation; tensors come back as typed arrays (F16 as a native Float16Array); big checkpoints can be opened lazily from a file, a Blob/File, or an HTTP URL with Range requests, reading only the tensors you ask for. Zero dependencies. Differential-tested against Python's safetensors (+ NumPy/PyTorch), in both directions.

Install

npm install @johnhenry/math-plus-safetensors

Quick start

import { openSafetensors, readSafetensors, writeSafetensors } from "@johnhenry/math-plus-safetensors";

// Lazy: reads the header only (one read), then one read per tensor.
const model = await openSafetensors("/path/to/model.safetensors"); // Node/Bun path
// or: await openSafetensors("https://huggingface.co/org/repo/resolve/main/model.safetensors",
//                            { headers: { Authorization: `Bearer ${token}` } });
// or: await openSafetensors(fileInput.files[0]);                   // Blob/File in a browser
model.names();                         // ["embeddings.weight", ...]
model.info("embeddings.weight");       // { dtype: "F16", shape: [50368, 768], dataOffsets: [...] }
const w = await model.read("embeddings.weight");   // Float16Array
const f = await model.toF32("embeddings.weight");  // new Float32Array
const all = await model.readMany();                // Map<name, TypedArray>, coalesced reads
await model.close();                               // closes files this package opened

// In memory: everything already in an ArrayBuffer/Uint8Array.
const file = readSafetensors(bytes);
file.view("x");   // zero-copy typed view (copied once if misaligned)
file.toF32("x");  // new Float32Array

// Write (byte-identical to Python's safetensors.serialize / save_file):
const out = writeSafetensors(
  { x: { dtype: "F32", shape: [2, 2], data: new Float32Array([1, 2, 3, 4]) } },
  { format: "pt" },
);

With @johnhenry/math-plus-tensor-core installed (optional peer):

import { toTensor, readTensor, fromTensor } from "@johnhenry/math-plus-safetensors/tensor";
const t = toTensor(file, "x");            // Tensor, aliases the file's bytes when aligned
const u = await readTensor(model, "x");   // Tensor from a lazy file
writeSafetensors({ x: fromTensor(t) });

API surface

| Group | Exports | |---|---| | In memory | readSafetensors(bytes) → SafetensorsFile {header, metadata, names(), has(), info(), bytes(), view(), toF32()} | | Lazy | openSafetensors(source, options?) → LazySafetensors {header, metadata, size, names(), has(), info(), readBytes(), read(), readMany(), toF32(), close()} | | Sources | toByteSource, MemorySource, BlobSource, HttpSource, FileHandleSource, the ByteSource interface (implement it for any other transport) | | Writer | writeSafetensors(tensors, metadata?) → Uint8Array | | Low level | parseHeader(bytes, {fileSize?}), headerLength, viewAs(dtype, bytes), toFloat32(dtype, bytes), aligned, BYTES, MAX_HEADER | | Errors | SafetensorsError with a code (InvalidOffset, MetadataIncompleteBuffer, DuplicateTensor, …; reference-implementation names where one exists) | | /tensor subpath | toTensor, readTensor, bytesToTensor, fromTensor (needs @johnhenry/math-plus-tensor-core) |

openSafetensors sources: Uint8Array/ArrayBuffer (zero-copy), Blob/File (incl. Bun.file()), URL or string starting with http:/https:/blob:/data:, a file: URL or any other string (a filesystem path — Node/Bun/Deno only), a Node FileHandle (not closed by us), or a custom ByteSource. Options: fetch, headers, signal, probeBytes (default 256 KiB — the first read, sized to cover the header in one round trip).

Validation

Mirrors the reference Rust implementation: 8-byte little-endian header length ≤ 100 MB; UTF-8 JSON that starts with {; string-only __metadata__ (or null, as MLX writes); known dtype, non-negative integer shape, and data_offsets spanning exactly prod(shape) × sizeof(dtype); offsets sorted by begin must tile the data section with no gap or overlap, starting at 0; and when the file size is known (in-memory, files, Blobs, HTTP with Content-Range) the last tensor must end exactly at end of file. Duplicate tensor names are rejected (plain JSON.parse would silently keep the last). Tensor order is preserved exactly as written, even for integer-like names.

Memory behaviour

  • readSafetensors keeps the whole file in memory; views are zero-copy when aligned. Files whose header isn't padded to 8 bytes (MLX writes them that way — e.g. the Laya checkpoints) make every tensor misaligned, so each view() copies that tensor once.
  • openSafetensors holds only the header. read/readBytes/toF32 read each tensor into a fresh (aligned, so zero-copy-viewable) buffer; readMany coalesces adjacent tensors (gap ≤ 64 KiB, chunk ≤ 64 MiB) and its views share those chunk buffers. Reading every tensor of an 842 MB F16 checkpoint therefore costs ~842 MB; toF32 on all of it ~1.7 GB.
  • HTTP: if the server ignores Range (answers 200), the whole body is downloaded once and served from memory (source.rangesSupported === false).
  • toFloat32/toF32 always return a new array you own (never aliasing the file), including for F32.

Limitations

  • dtypes: BOOL, U8/I8, U16/I16, F16, BF16, U32/I32, F32, U64/I64, F64. FP8 (F8_E4M3, F8_E5M2, …), F4/F6, and C64 are rejected with InvalidDtype (the whole file, not just that tensor).
  • F16 needs Float16Array (Node ≥ 24, Bun, Deno, Chrome 135+, Firefox 129+, Safari 18.2+); without it view/read/toF32 of F16 throw Float16Unavailable. BF16 has no native array: views are Uint16Array bit patterns; toF32 widens exactly.
  • toF32 of I64/U64 goes through Number, so magnitudes above 2^53 round twice (to double, then to float32).
  • Host byte order is assumed little-endian (true on every platform JS engines ship on today); data is never byte-swapped.
  • The writer builds the whole file in one Uint8Array (no streaming writer) and follows the reference layout: tensors sorted by dtype alignment then name, header space-padded to 8 bytes.
  • No resumable/parallel HTTP downloads, no retry policy, no caching — combine with a caching layer (e.g. @johnhenry/hf-cache) for that. Redirects are followed by fetch; the Range header is re-sent to the final URL (standard fetch behaviour).
  • JSR publishes the main entry only (./src/index.ts); the /tensor subpath is npm-only for now.

Tests

npm test. test/oracle.test.ts generates fixtures with Python (scripts/safetensors_oracle.py: safetensors.numpy.save_file, safetensors.torch.save_file for BF16, and a hand-built unpadded MLX-style file) and checks bytes and float32 conversions bit-for-bit, then checks our writer is read back by Python and is byte-identical to safetensors.serialize. It skips (never fails) without a Python that has numpy + safetensors ($MATH_PLUS_ORACLE_PYTHON, else python3); BF16 fixtures additionally need torch or ml_dtypes. test/checkpoint.test.ts lazily opens the real aac6fef/laya-mlx checkpoint from the local Hugging Face cache when present (skipped otherwise).