File size: 7,460 Bytes
30bafb7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | // Hugging Face Hub client — fetch only the bytes this device needs.
//
// Two rules shape this file.
//
// 1. NOTHING is downloaded whole. config.json and the tokenizer are small and
// fetched normally; the weights are read with HTTP range requests against
// the byte offsets in the safetensors header. A device that owns four
// layers transfers four layers.
//
// 2. The token is asked for ONCE, held in memory, and never written anywhere.
// Not localStorage, not sessionStorage, not a cookie, not a URL, not the
// log, and above all never onto the wire — peers each authenticate
// themselves. A page reload is meant to lose it. See requestToken() in
// app.js for the prompt; this file only ever receives it as an argument.
(function (root) {
"use strict";
let ST; // Safetensors — resolved per environment at the end
const HUB = "https://huggingface.co";
// A token that reaches a log or a peer is a leaked credential, so anything
// that formats an error goes through here first. Cheap, and it means a
// careless template literal cannot undo rule 2.
function redact(s, token) {
if (!token) return s;
return String(s).split(token).join("hf_***");
}
function headers(token, extra) {
const h = Object.assign({}, extra || {});
if (token) h["Authorization"] = "Bearer " + token;
return h;
}
function fileUrl(repo, file, revision) {
return `${HUB}/${repo}/resolve/${encodeURIComponent(revision || "main")}/${file}`;
}
// Turn the Hub's status codes into something a person can act on. 401/403 on
// a public-looking repo almost always means "gated, accept the licence" or
// "private, needs a token", and those need different actions from the user,
// so they are not collapsed into one message.
class HFError extends Error {
constructor(msg, kind, status) { super(msg); this.kind = kind; this.status = status; }
}
function statusError(res, repo, file, hasToken) {
const where = `${repo}/${file}`;
if (res.status === 401)
return new HFError(hasToken
? `${where}: the token was rejected (401). Check it is a valid READ token and has not been revoked.`
: `${where} needs authentication (401).`, "auth", 401);
if (res.status === 403)
return new HFError(hasToken
? `${where}: this token does not have access (403). If the model is gated, accept its licence on the model page first.`
: `${where} is gated or private (403).`, "auth", 403);
if (res.status === 404)
return new HFError(`${where} not found (404). Check the repo id, and that this model ships .safetensors weights.`, "missing", 404);
return new HFError(`${where}: HTTP ${res.status}`, "http", res.status);
}
async function getJSON(repo, file, revision, token) {
const res = await fetch(fileUrl(repo, file, revision), { headers: headers(token) });
if (!res.ok) throw statusError(res, repo, file, !!token);
try { return await res.json(); }
catch (e) { throw new HFError(`${repo}/${file} is not valid JSON`, "parse", res.status); }
}
// Range read. The Hub redirects to a CDN that honours Range and answers 206;
// a 200 here means the whole file came back, which for a multi-hundred-MB
// weights file is exactly the outcome this project exists to avoid — so it
// is treated as an error rather than silently accepted.
async function getRange(repo, file, revision, token, start, end) {
const res = await fetch(fileUrl(repo, file, revision), {
headers: headers(token, { Range: `bytes=${start}-${end - 1}` }),
});
if (res.status === 200)
throw new HFError(`${repo}/${file}: the server ignored the byte range and returned the whole file — refusing it.`, "norange", 200);
if (!res.ok && res.status !== 206) throw statusError(res, repo, file, !!token);
return res.arrayBuffer();
}
// ---- what a repo contains ---------------------------------------------------
// Sharded repos carry model.safetensors.index.json mapping tensor -> shard.
// Single-file repos just have model.safetensors. Try the index first: its
// absence is the normal case and a 404 is cheap.
async function listWeightFiles(repo, revision, token) {
try {
const idx = await getJSON(repo, "model.safetensors.index.json", revision, token);
const files = [...new Set(Object.values(idx.weight_map || {}))];
if (!files.length) throw new HFError("weight index lists no shards", "parse", 200);
return { files, weightMap: idx.weight_map };
} catch (e) {
if (e.kind === "auth") throw e; // don't mask a token problem
return { files: ["model.safetensors"], weightMap: null };
}
}
// Read just the header of each weight shard: two requests per shard, a few
// tens of KB, and afterwards we know every tensor's exact byte range without
// having touched a single weight.
async function readIndex(repo, revision, token, onProgress) {
const { files } = await listWeightFiles(repo, revision, token);
const tensors = new Map();
for (const f of files) {
if (onProgress) onProgress(`reading ${f} header…`);
const head = await getRange(repo, f, revision, token, 0, 8);
const dv = new DataView(head);
const lo = dv.getUint32(0, true), hi = dv.getUint32(4, true);
if (hi !== 0) throw new HFError(`${f}: implausible header length — not a safetensors file`, "parse", 200);
const full = await getRange(repo, f, revision, token, 0, 8 + lo);
const parsed = ST.parseHeader(full);
for (const [name, t] of parsed.tensors) tensors.set(name, Object.assign({ file: f }, t));
}
return { files, tensors };
}
// Fetch a specific set of tensors as f32, coalescing adjacent byte ranges.
// This is the only function that moves real weight data.
async function fetchTensors(repo, revision, token, tensors, onProgress) {
const out = new Map();
const byFile = new Map();
for (const t of tensors) {
if (!byFile.has(t.file)) byFile.set(t.file, []);
byFile.get(t.file).push(t);
}
let done = 0;
const total = tensors.reduce((a, t) => a + t.bytes, 0);
for (const [file, list] of byFile) {
for (const run of ST.coalesce(list)) {
const buf = await getRange(repo, file, revision, token, run.start, run.end);
for (const t of run.tensors) {
const off = t.start - run.start;
out.set(t.name, ST.toF32(t.dtype, new Uint8Array(buf, off, t.bytes)));
done += t.bytes;
if (onProgress) onProgress(`${(done / 1048576).toFixed(1)} / ${(total / 1048576).toFixed(1)} MB`);
}
}
}
return out;
}
// A cheap probe so the UI can ask for a token only when one is actually
// needed, instead of demanding credentials up front for public models.
async function needsAuth(repo, revision) {
try {
const res = await fetch(fileUrl(repo, "config.json", revision), { method: "HEAD" });
return res.status === 401 || res.status === 403;
} catch (e) { return false; }
}
const api = { HUB, HFError, fileUrl, getJSON, getRange, listWeightFiles, readIndex,
fetchTensors, needsAuth, redact };
if (typeof module !== "undefined" && module.exports) { ST = require("./safetensors.js"); module.exports = api; }
else { ST = root.Safetensors; root.HF = api; }
})(typeof self !== "undefined" ? self : this);
|