| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { ERROR, EngineError } from "./errors.js"; |
| import { |
| CHAT_CONFIG, |
| CONTENT_TYPES, |
| LEGACY_TENSOR_MANIFEST, |
| TENSOR_MANIFEST, |
| } from "./ingest.js"; |
| import { CACHE_CONFIG, CACHE_MODEL, CACHE_WASM, isInjected } from "./model-store.js"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function resolveModelUrl(modelUrl) { |
| let url = modelUrl + (modelUrl.endsWith("/") ? "" : "/"); |
| if (!url.match(/.+\/resolve\/.+\//)) url += "resolve/main/"; |
| return new URL(url).href; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function prefetchModel({ |
| modelId, |
| record, |
| onProgress = () => {}, |
| signal, |
| fetchImpl = globalThis.fetch, |
| }) { |
| if (!record?.model_lib) { |
| throw new EngineError( |
| ERROR.BAD_REQUEST, |
| `"${modelId}" has no \`model_lib\`, so there is nothing to prefetch from. ` + |
| "A remote source needs one; see load(url, { modelLib }).", |
| { modelId }, |
| ); |
| } |
|
|
| const base = resolveModelUrl(record.model); |
| const get = async (url, what) => { |
| if (signal?.aborted) throw aborted(modelId); |
| const res = await fetchImpl(url, signal ? { signal } : undefined).catch((err) => { |
| throw new EngineError(ERROR.GENERATION_FAILED, `Prefetch could not reach ${what}: ${err?.message ?? err}`, { |
| modelId, |
| url, |
| }); |
| }); |
| if (!res.ok) { |
| throw new EngineError( |
| ERROR.UNKNOWN_MODEL, |
| `Prefetch got ${res.status} for ${what} at ${url}. ` + |
| "Check the model's base URL — a 404 here usually means the id or the URL is wrong.", |
| { modelId, url, status: res.status }, |
| ); |
| } |
| return res; |
| }; |
|
|
| |
| |
| onProgress({ phase: "manifest", done: 0, total: 1, label: CHAT_CONFIG }); |
| const configRes = await get(base + CHAT_CONFIG, CHAT_CONFIG); |
| const configBytes = await configRes.arrayBuffer(); |
| const chatConfig = parseJson(configBytes, CHAT_CONFIG, modelId); |
|
|
| |
| let manifestName = TENSOR_MANIFEST; |
| let manifestRes = await fetchImpl(base + TENSOR_MANIFEST, signal ? { signal } : undefined).catch(() => null); |
| if (!manifestRes?.ok) { |
| manifestName = LEGACY_TENSOR_MANIFEST; |
| manifestRes = await get(base + LEGACY_TENSOR_MANIFEST, "the weight index"); |
| } |
| const manifestBytes = await manifestRes.arrayBuffer(); |
| const manifest = parseJson(manifestBytes, manifestName, modelId); |
|
|
| const shards = (manifest.records ?? []).map((r) => r.dataPath).filter(Boolean); |
| if (shards.length === 0) { |
| throw new EngineError( |
| ERROR.UNKNOWN_MODEL, |
| `${manifestName} at ${base} lists no weight shards, so this is not an MLC model directory.`, |
| { modelId, url: base + manifestName }, |
| ); |
| } |
|
|
| const tokenizers = (Array.isArray(chatConfig.tokenizer_files) ? chatConfig.tokenizer_files : []).filter( |
| (n) => n === "tokenizer.json" || n === "tokenizer.model", |
| ); |
|
|
| const plan = [ |
| { scope: CACHE_CONFIG, url: base + CHAT_CONFIG, body: configBytes, type: CONTENT_TYPES.json }, |
| { scope: CACHE_MODEL, url: base + manifestName, body: manifestBytes, type: CONTENT_TYPES.json }, |
| ...tokenizers.map((name) => ({ |
| scope: CACHE_MODEL, |
| url: base + name, |
| type: name.endsWith(".json") ? CONTENT_TYPES.json : CONTENT_TYPES.bin, |
| })), |
| ...shards.map((p) => ({ scope: CACHE_MODEL, url: new URL(p, base).href, type: CONTENT_TYPES.bin })), |
| |
| |
| { scope: CACHE_WASM, url: record.model_lib, type: CONTENT_TYPES.wasm }, |
| ]; |
|
|
| const openCaches = new Map(); |
| const cacheFor = async (scope) => { |
| if (!openCaches.has(scope)) openCaches.set(scope, await caches.open(scope)); |
| return openCaches.get(scope); |
| }; |
|
|
| let bytes = 0; |
| let done = 0; |
| for (const item of plan) { |
| if (signal?.aborted) throw aborted(modelId); |
| const cache = await cacheFor(item.scope); |
| onProgress({ phase: "downloading", done, total: plan.length, label: basename(item.url) }); |
|
|
| |
| |
| if (item.body === undefined && (await cache.match(new Request(item.url)))) { |
| done += 1; |
| continue; |
| } |
|
|
| const body = item.body ?? (await (await get(item.url, basename(item.url))).arrayBuffer()); |
| bytes += body.byteLength; |
| await cache.put( |
| new Request(item.url), |
| new Response(body, { status: 200, headers: { "Content-Type": item.type } }), |
| ); |
| done += 1; |
| } |
| onProgress({ phase: "downloading", done, total: plan.length, label: "done" }); |
|
|
| return { modelId, files: plan.length, bytes, alreadyCached: false }; |
| } |
|
|
| const aborted = (modelId) => |
| new EngineError(ERROR.ABORTED, `Prefetch of "${modelId}" was aborted.`, { modelId }); |
|
|
| function parseJson(buffer, what, modelId) { |
| try { |
| return JSON.parse(new TextDecoder().decode(buffer)); |
| } catch { |
| throw new EngineError( |
| ERROR.UNKNOWN_MODEL, |
| `${what} for "${modelId}" is not valid JSON — the URL is probably not an MLC model directory.`, |
| { modelId, what }, |
| ); |
| } |
| } |
|
|
| const basename = (url) => url.split("/").pop() || url; |
|
|
| |
| export const isAlreadyLocal = (record) => isInjected(record); |
|
|