{"version":3,"file":"model_source.cjs","names":[],"sources":["../../../../src/batteries/llm/transformers_js/model_source.ts"],"sourcesContent":["/**\n * Custom model-source resolver for transformers.js — the dual-environment seam that lets a consumer\n * serve a model's files (each submodule ONNX, the tokenizer, the config) from anywhere: OPFS, a\n * different repo per modality, bundled bytes, an in-memory map.\n *\n * @module @nhtio/adk/batteries/llm/transformers_js/model_source\n *\n * @remarks\n * **Mechanism (verified against the installed `@huggingface/transformers` build, NODE + WEB, 8 refs\n * each — ungated by `IS_NODE_ENV`):** transformers.js routes every model file through\n * `env.customCache.match(key)` when `env.useCustomCache === true`. `match` may return a `Response`\n * (bytes), a `string` (a path/URL the loader then fetches), or `undefined` (fall through to the normal\n * HF download). This module installs a `customCache` whose `match` parses the loader's key back into\n * `{repo, filename}` and delegates to the user's {@link TransformersJsModelSource} hook.\n *\n * **The cache key is the REMOTE URL, not `${repo}/${filename}` (corrected at impl from the plan's\n * assumption).** `buildResourcePaths` computes `remoteURL = pathJoin(env.remoteHost,\n * env.remotePathTemplate.replace('{model}', repo).replace('{revision}', rev), filename)` →\n * `https://huggingface.co/<repo>/resolve/<rev>/<path/to/file>`. `tryCache` first probes the *local*\n * path (no `/resolve/` segment — our parser returns `undefined`, so it correctly falls through) and\n * then this remote URL. {@link parseResourceKey} reverses exactly that template.\n *\n * **`env` is a process-global singleton.** {@link withModelSource} sets `useCustomCache`/`customCache`\n * for the duration of one load and restores the previous values after — behind a module-level async\n * mutex so concurrent loads across adapters never observe each other's hook. {@link installModelSource}\n * is the lower-level set-and-return-a-restore-fn primitive for callers that manage their own scope.\n *\n * The resolver does NOT implement OPFS/bundled reading — it is the plug; the consumer's hook is the\n * implementation. Reused verbatim by the embeddings battery (same `env` mechanism).\n */\n\nimport { isInstanceOf } from '@nhtio/adk/guards'\nimport type { TransformersJsModelSource } from './types'\n\n/** The cache shape transformers.js requires (`match` + `put`, Web Cache API subset). */\ninterface TransformersCacheLike {\n  match: (request: string) => Promise<Response | string | undefined>\n  put: (request: string, response: Response) => Promise<void>\n}\n\n/** The mutable `env` surface we touch — kept minimal + structurally typed (no peer import here). */\ninterface TransformersEnvLike {\n  useCustomCache: boolean\n  customCache: TransformersCacheLike | null\n  remoteHost?: string\n  remotePathTemplate?: string\n}\n\nconst DEFAULT_REMOTE_HOST = 'https://huggingface.co/'\nconst DEFAULT_REMOTE_PATH_TEMPLATE = '{model}/resolve/{revision}/'\n\n/**\n * Reverse `buildResourcePaths`' remote-URL key back into `{repo, filename}`.\n *\n * Handles the canonical `{host}{model}/resolve/{revision}/{filename}` template. Returns `undefined`\n * for any key that is not a remote-host URL (e.g. the local-path probe `tryCache` issues first), so the\n * caller falls through to the default loader instead of mis-routing.\n *\n * @param key - The string transformers.js passes to `cache.match` (the remote URL).\n * @param env - The (possibly customized) host/template; defaults match the library's own defaults.\n */\nexport const parseResourceKey = (\n  key: string,\n  env: { remoteHost?: string; remotePathTemplate?: string } = {}\n): { repo: string; filename: string } | undefined => {\n  const host = env.remoteHost ?? DEFAULT_REMOTE_HOST\n  const template = env.remotePathTemplate ?? DEFAULT_REMOTE_PATH_TEMPLATE\n  if (!key.startsWith(host)) return undefined\n  const rest = key.slice(host.length)\n  // template is `{model}/resolve/{revision}/` → the literal middle segment is `/resolve/`.\n  const literal = template.replace('{model}', '').replace('{revision}', '')\n  // literal is `/resolve//` collapse the doubled slash to the real delimiter `/resolve/`.\n  const delimiter = literal.replace(/\\/+/g, '/')\n  const idx = rest.indexOf(delimiter)\n  if (idx === -1) return undefined\n  const repo = rest.slice(0, idx)\n  const afterDelim = rest.slice(idx + delimiter.length)\n  // afterDelim is `{revision}/{filename}` — strip the first path segment (the revision).\n  const slash = afterDelim.indexOf('/')\n  if (slash === -1) return undefined\n  const filename = afterDelim.slice(slash + 1)\n  if (!repo || !filename) return undefined\n  return { repo, filename }\n}\n\n/**\n * Wrap a {@link TransformersJsModelSource} hook into a Web-Cache-API-compatible object suitable for\n * `env.customCache`. `match` parses the key, calls the hook, and normalizes the result:\n * `Uint8Array` → `new Response(bytes)`; `string`/`Response` pass through; `undefined`/throw → fall\n * through. `put` is a no-op (served files are never re-cached — `toCacheResponse` is false for them).\n *\n * @param hook - The consumer's resolver.\n * @param env - Host/template for key parsing (defaults to the library defaults).\n */\nexport const modelSourceToCache = (\n  hook: TransformersJsModelSource,\n  env: { remoteHost?: string; remotePathTemplate?: string } = {}\n): TransformersCacheLike => ({\n  match: async (request: string): Promise<Response | string | undefined> => {\n    const parsed = parseResourceKey(request, env)\n    if (!parsed) return undefined\n    let result: Uint8Array | string | Response | undefined\n    try {\n      result = await hook(parsed)\n    } catch {\n      // A throwing hook must not abort the load — fall through to the normal HF fetch.\n      return undefined\n    }\n    if (result === undefined) return undefined\n    if (typeof result === 'string') return result\n    if (isInstanceOf(result, 'Response', Response)) return result\n    // Uint8Array → Response. Cast keeps us off a hard DOM/Node BodyInit lib dependency.\n    return new Response(result as unknown as BodyInit)\n  },\n  // No-op: served resources are not re-stored (the loader only caches its own remote downloads).\n  put: async (): Promise<void> => undefined,\n})\n\n// ── Module-level async mutex (env is a global singleton) ───────────────────────────────────────────\nlet chain: Promise<unknown> = Promise.resolve()\n\n/**\n * Install a model-source hook on `env` and return a restore function. Sets `useCustomCache = true` +\n * `customCache`, capturing the previous values. The returned `restore()` puts them back. NOT mutex-\n * guarded on its own — use {@link withModelSource} for the scoped, serialized form.\n *\n * @param env - The transformers.js `env` object (from `await import('@huggingface/transformers')`).\n * @param hook - The resolver to install.\n * @returns A function that restores `env`'s prior cache configuration.\n */\nexport const installModelSource = (\n  env: TransformersEnvLike,\n  hook: TransformersJsModelSource\n): (() => void) => {\n  const prevUse = env.useCustomCache\n  const prevCache = env.customCache\n  env.customCache = modelSourceToCache(hook, {\n    remoteHost: env.remoteHost,\n    remotePathTemplate: env.remotePathTemplate,\n  })\n  env.useCustomCache = true\n  return () => {\n    env.useCustomCache = prevUse\n    env.customCache = prevCache\n  }\n}\n\n/**\n * Run `load()` with `hook` installed on `env`, then restore — serialized against every other\n * `withModelSource` call so concurrent adapter loads never clobber the global `env`. The hook is only\n * active for the duration of `load()`; after it resolves (or rejects) the prior `env` cache config is\n * restored even on error.\n *\n * @param env - The transformers.js `env` object.\n * @param hook - The resolver to install for this load.\n * @param load - The async load operation (e.g. `pipeline(...)` / `from_pretrained(...)`).\n */\nexport const withModelSource = async <T>(\n  env: TransformersEnvLike,\n  hook: TransformersJsModelSource,\n  load: () => Promise<T>\n): Promise<T> => {\n  const run = chain.then(async () => {\n    const restore = installModelSource(env, hook)\n    try {\n      return await load()\n    } finally {\n      restore()\n    }\n  })\n  // Keep the chain alive regardless of this run's outcome (swallow here; caller still sees the result).\n  chain = run.then(\n    () => undefined,\n    () => undefined\n  )\n  return run\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAM,sBAAsB;AAC5B,IAAM,+BAA+B;;;;;;;;;;;AAYrC,IAAa,oBACX,KACA,MAA4D,CAAC,MACV;CACnD,MAAM,OAAO,IAAI,cAAc;CAC/B,MAAM,WAAW,IAAI,sBAAsB;CAC3C,IAAI,CAAC,IAAI,WAAW,IAAI,GAAG,OAAO,KAAA;CAClC,MAAM,OAAO,IAAI,MAAM,KAAK,MAAM;CAIlC,MAAM,YAFU,SAAS,QAAQ,WAAW,EAAE,EAAE,QAAQ,cAAc,EAEpD,EAAQ,QAAQ,QAAQ,GAAG;CAC7C,MAAM,MAAM,KAAK,QAAQ,SAAS;CAClC,IAAI,QAAQ,IAAI,OAAO,KAAA;CACvB,MAAM,OAAO,KAAK,MAAM,GAAG,GAAG;CAC9B,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU,MAAM;CAEpD,MAAM,QAAQ,WAAW,QAAQ,GAAG;CACpC,IAAI,UAAU,IAAI,OAAO,KAAA;CACzB,MAAM,WAAW,WAAW,MAAM,QAAQ,CAAC;CAC3C,IAAI,CAAC,QAAQ,CAAC,UAAU,OAAO,KAAA;CAC/B,OAAO;EAAE;EAAM;CAAS;AAC1B;;;;;;;;;;AAWA,IAAa,sBACX,MACA,MAA4D,CAAC,OAClC;CAC3B,OAAO,OAAO,YAA4D;EACxE,MAAM,SAAS,iBAAiB,SAAS,GAAG;EAC5C,IAAI,CAAC,QAAQ,OAAO,KAAA;EACpB,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,KAAK,MAAM;EAC5B,QAAQ;GAEN;EACF;EACA,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;EACjC,IAAI,OAAO,WAAW,UAAU,OAAO;EACvC,IAAI,eAAA,aAAa,QAAQ,YAAY,QAAQ,GAAG,OAAO;EAEvD,OAAO,IAAI,SAAS,MAA6B;CACnD;CAEA,KAAK,YAA2B,KAAA;AAClC;AAGA,IAAI,QAA0B,QAAQ,QAAQ;;;;;;;;;;AAW9C,IAAa,sBACX,KACA,SACiB;CACjB,MAAM,UAAU,IAAI;CACpB,MAAM,YAAY,IAAI;CACtB,IAAI,cAAc,mBAAmB,MAAM;EACzC,YAAY,IAAI;EAChB,oBAAoB,IAAI;CAC1B,CAAC;CACD,IAAI,iBAAiB;CACrB,aAAa;EACX,IAAI,iBAAiB;EACrB,IAAI,cAAc;CACpB;AACF;;;;;;;;;;;AAYA,IAAa,kBAAkB,OAC7B,KACA,MACA,SACe;CACf,MAAM,MAAM,MAAM,KAAK,YAAY;EACjC,MAAM,UAAU,mBAAmB,KAAK,IAAI;EAC5C,IAAI;GACF,OAAO,MAAM,KAAK;EACpB,UAAU;GACR,QAAQ;EACV;CACF,CAAC;CAED,QAAQ,IAAI,WACJ,KAAA,SACA,KAAA,CACR;CACA,OAAO;AACT"}