{"version":3,"file":"adapter.mjs","names":["#options","#pipeline","#resolvePipeline","#pipelinePromise"],"sources":["../../../../src/batteries/embeddings/transformers_js/adapter.ts"],"sourcesContent":["/**\n * transformers.js (ONNX, dual-environment) Embeddings adapter battery.\n *\n * @module @nhtio/adk/batteries/embeddings/transformers_js/adapter\n *\n * @remarks\n * Embeddings battery backed by transformers.js's `feature-extraction` pipeline. **Environment-neutral**\n * — runs in Node (via `onnxruntime-node`) and the browser (via `onnxruntime-web` / WebGPU), auto-\n * selected by the package; there is no WebGPU requirement, so this battery is surfaced from the\n * environment-neutral `@nhtio/adk/batteries/embeddings` barrel alongside the OpenAI one.\n *\n * Same user-facing surface as the OpenAI / WebLLM embeddings batteries (`isAvailable` / `dimensions` /\n * `preload` / `reset` / `embed` / `embedMany`), same `number[]` return shape, same query/document\n * prefix handling (the shared `applyEmbeddingPrefix`).\n *\n * `@huggingface/transformers` is an optional peer dependency, imported lazily.\n *\n * **Cross-runtime vector caveat:** embeddings produced here are not guaranteed bit-identical to those\n * from a different runtime (WebLLM/MLC, or even node-ONNX vs web-ONNX for the same model). A vector\n * corpus must be embedded AND queried by one backend.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { poolAndNormalize } from './pooling'\nimport { validateOptions } from './validation'\nimport { applyEmbeddingPrefix } from '../openai/helpers'\nimport { emitLifecycle } from '../../llm/chat_common/lifecycle'\nimport { withModelSource } from '../../llm/transformers_js/model_source'\nimport {\n  E_INVALID_TRANSFORMERS_JS_EMBEDDINGS_OPTIONS,\n  E_TRANSFORMERS_JS_EMBEDDINGS_ENGINE_ERROR,\n} from './exceptions'\nimport type { EmbedOptions } from '../openai/types'\nimport type {\n  TransformersJsEmbeddingsAdapterOptions,\n  TransformersJsEmbeddingsPipeline,\n  CreateTransformersJsEmbeddingsPipeline,\n} from './types'\n\nconst makeDefaultCreatePipeline = (\n  modelSource: TransformersJsEmbeddingsAdapterOptions['modelSource']\n): CreateTransformersJsEmbeddingsPipeline => {\n  return async ({ model, device, dtype, onInitProgress }) => {\n    const transformers = await import('@huggingface/transformers')\n    const { pipeline, env } = transformers\n    const load = async () =>\n      (await pipeline('feature-extraction', model, {\n        ...(device ? { device } : {}),\n        ...(dtype ? { dtype } : {}),\n        ...(onInitProgress ? { progress_callback: onInitProgress } : {}),\n      } as never)) as unknown as TransformersJsEmbeddingsPipeline\n    // When a custom model source is configured, serve files through it behind the global-`env` mutex.\n    return modelSource ? withModelSource(env as never, modelSource, load) : load()\n  }\n}\n\n/**\n * Embeddings adapter for transformers.js's feature-extraction pipeline.\n *\n * @remarks\n * Reusable: construct once, call {@link TransformersJsEmbeddingsAdapter.embed} / {@link embedMany} as\n * many times as needed. The pipeline is resolved lazily on first use (or via {@link preload}) and\n * cached with single-flight semantics so concurrent calls share one load.\n */\nexport class TransformersJsEmbeddingsAdapter {\n  readonly #options: TransformersJsEmbeddingsAdapterOptions\n  #pipeline: TransformersJsEmbeddingsPipeline | undefined\n  #pipelinePromise: Promise<TransformersJsEmbeddingsPipeline> | undefined\n\n  /**\n   * Whether this battery is available. transformers.js is environment-neutral (Node + browser), so\n   * this is `true` whenever the runtime can import the peer — there is no WebGPU requirement.\n   */\n  public static isAvailable(): boolean {\n    return true\n  }\n\n  /**\n   * @param options - Constructor options. Validated eagerly.\n   * @throws {@link @nhtio/adk/batteries!E_INVALID_TRANSFORMERS_JS_EMBEDDINGS_OPTIONS} when invalid.\n   */\n  constructor(options: unknown) {\n    this.#options = validateOptions(options)\n    this.#pipeline = this.#options.pipeline\n  }\n\n  /** Declared output dimensionality (from options), or `undefined` if not configured. */\n  get dimensions(): number | undefined {\n    return this.#options.dimensions\n  }\n\n  /** Instance availability probe (honours an injected `isAvailable`). */\n  isAvailable(): boolean {\n    return (this.#options.isAvailable ?? TransformersJsEmbeddingsAdapter.isAvailable)()\n  }\n\n  /** Eagerly loads (and caches) the pipeline so the first `embed` call is fast. Idempotent. */\n  async preload(): Promise<void> {\n    await this.#resolvePipeline()\n  }\n\n  /** Drops the cached pipeline and in-flight load so the next call reloads. */\n  reset(): void {\n    this.#pipeline = undefined\n    this.#pipelinePromise = undefined\n  }\n\n  /**\n   * Release the loaded model's ONNX sessions + GPU/wasm buffers, then drop the cached pipeline.\n   *\n   * @remarks\n   * `reset()` only nulls the JS reference; the native ONNX Runtime sessions and WebGPU/wasm device memory\n   * stay alive until GC. Loading many embedding models back-to-back in one browser session (e.g. a full\n   * matrix run) accumulates those sessions until the heap is exhausted. `FeatureExtractionPipeline`\n   * extends `Pipeline`, which exposes `dispose()` — this awaits it so the memory is reclaimed between\n   * loads, swallows a disposal error (teardown must not throw), and finishes with `reset()`. Idempotent.\n   */\n  async dispose(): Promise<void> {\n    const pipeline = this.#pipeline ?? (await this.#pipelinePromise?.catch(() => undefined))\n    const pipeWithDispose = pipeline as { dispose?: () => Promise<unknown> } | undefined\n    if (typeof pipeWithDispose?.dispose === 'function') {\n      await Promise.resolve(pipeWithDispose.dispose()).catch(() => undefined)\n    }\n    this.reset()\n  }\n\n  async #resolvePipeline(): Promise<TransformersJsEmbeddingsPipeline> {\n    if (this.#pipeline) return this.#pipeline\n    if (!this.isAvailable()) {\n      throw new E_INVALID_TRANSFORMERS_JS_EMBEDDINGS_OPTIONS([\n        'the transformers.js embeddings battery is not available in this runtime',\n      ])\n    }\n    const opts = this.#options\n    this.#pipelinePromise ??= (async () => {\n      emitLifecycle(opts, 'transformers_js_embed', opts.model, 'loading', {\n        detail: 'loading feature-extraction pipeline',\n      })\n      // Forward each provider download event into a normalized `loading` lifecycle report.\n      const hasLifecycle =\n        opts.onLifecycle ?? opts.onLoading ?? opts.onReady ?? opts.onGenerating ?? opts.onError\n      const forwardedInitProgress = hasLifecycle\n        ? (info: unknown) => {\n            const p = (info as { progress?: number } | undefined)?.progress\n            emitLifecycle(opts, 'transformers_js_embed', opts.model, 'loading', {\n              ...(typeof p === 'number' ? { progress: p / 100 } : {}),\n              raw: info,\n            })\n            opts.onInitProgress?.(info as never)\n          }\n        : opts.onInitProgress\n      const createPipeline = opts.createPipeline ?? makeDefaultCreatePipeline(opts.modelSource)\n      try {\n        // `from_pretrained` covers both fetch (reported via progress_callback → `loading`) and the\n        // ONNX-graph / WebGPU-WASM warmup. Mark the latter as `compiling` — a COARSE upper-bound marker\n        // (fetch + compile overlap inside the call), consistent with the LLM batteries.\n        emitLifecycle(opts, 'transformers_js_embed', opts.model, 'compiling', {\n          detail: 'compiling feature-extraction graph',\n        })\n        const pipe = await createPipeline({\n          model: opts.model,\n          device: opts.device,\n          dtype: opts.dtype,\n          onInitProgress: forwardedInitProgress,\n        })\n        this.#pipeline = pipe\n        emitLifecycle(opts, 'transformers_js_embed', opts.model, 'ready', {\n          detail: 'feature-extraction pipeline ready',\n        })\n        return pipe\n      } catch (err) {\n        this.#pipelinePromise = undefined\n        emitLifecycle(opts, 'transformers_js_embed', opts.model, 'error', { error: err })\n        throw new E_TRANSFORMERS_JS_EMBEDDINGS_ENGINE_ERROR([\n          `could not load the transformers.js pipeline: ${isError(err) ? err.message : String(err)} — install the peer dependency (pnpm add @huggingface/transformers)`,\n        ])\n      }\n    })()\n    return this.#pipelinePromise\n  }\n\n  /**\n   * Embeds a single string.\n   *\n   * @param text - The input text.\n   * @param opts - Per-call options (`kind`).\n   * @returns The embedding vector as a plain `number[]`.\n   */\n  async embed(text: string, opts?: EmbedOptions): Promise<number[]> {\n    const [vec] = await this.embedMany([text], opts)\n    return vec\n  }\n\n  /**\n   * Embeds a batch of strings in a single pipeline call.\n   *\n   * @param texts - The input texts.\n   * @param opts - Per-call options (`kind`). Defaults to `kind: 'document'`.\n   * @returns One embedding vector per input, in input order, each a plain `number[]`.\n   * @throws {@link @nhtio/adk/batteries!E_TRANSFORMERS_JS_EMBEDDINGS_ENGINE_ERROR} when the call fails\n   *   or returns a malformed result.\n   */\n  async embedMany(texts: string[], opts?: EmbedOptions): Promise<number[][]> {\n    if (texts.length === 0) return []\n    const kind = opts?.kind ?? 'document'\n    const input = applyEmbeddingPrefix(texts, kind, this.#options)\n\n    const pipe = await this.#resolvePipeline()\n    const pooling = this.#options.pooling ?? 'mean'\n    const normalize = this.#options.normalize ?? true\n    const battery = (this.#options.poolingOwner ?? 'engine') === 'battery'\n\n    emitLifecycle(this.#options, 'transformers_js_embed', this.#options.model, 'generating')\n\n    let tensor: { tolist: () => unknown; dims?: number[] }\n    try {\n      tensor = (await (pipe as unknown as (i: unknown, o: unknown) => Promise<unknown>)(\n        input,\n        // 'battery' owner: request RAW token states (no engine pooling/normalize) and do it ourselves\n        // in deterministic JS. 'engine' owner: delegate to the pipeline exactly as before.\n        battery ? { pooling: 'none' } : { pooling, normalize }\n      )) as { tolist: () => unknown; dims?: number[] }\n    } catch (err) {\n      emitLifecycle(this.#options, 'transformers_js_embed', this.#options.model, 'error', {\n        error: err,\n      })\n      throw new E_TRANSFORMERS_JS_EMBEDDINGS_ENGINE_ERROR([\n        isError(err) ? err.message : String(err),\n      ])\n    }\n\n    if (!tensor || typeof tensor.tolist !== 'function') {\n      throw new E_TRANSFORMERS_JS_EMBEDDINGS_ENGINE_ERROR([\n        'feature-extraction returned a non-Tensor result',\n      ])\n    }\n\n    const list = tensor.tolist()\n    let vectors: number[][]\n    if (battery) {\n      // Raw states: tolist() → [batch, seq, hidden]. A single ungrouped input may come back as\n      // [seq, hidden] → wrap to a batch of one. Pool + normalize deterministically.\n      const states = list as unknown[]\n      const tokenStates = (\n        Array.isArray(states) &&\n        Array.isArray(states[0]) &&\n        Array.isArray((states[0] as unknown[])[0])\n          ? states\n          : [states]\n      ) as number[][][]\n      vectors = poolAndNormalize(tokenStates, pooling, normalize)\n    } else {\n      // With engine pooling, the Tensor is [batch, hidden] → tolist() yields number[][].\n      vectors =\n        Array.isArray(list) && Array.isArray(list[0]) ? (list as number[][]) : [list as number[]]\n    }\n\n    if (vectors.length !== input.length) {\n      emitLifecycle(this.#options, 'transformers_js_embed', this.#options.model, 'error', {\n        error: new Error(`expected ${input.length} vectors, got ${vectors.length}`),\n      })\n      throw new E_TRANSFORMERS_JS_EMBEDDINGS_ENGINE_ERROR([\n        `expected ${input.length} vectors, got ${vectors.length}`,\n      ])\n    }\n    emitLifecycle(this.#options, 'transformers_js_embed', this.#options.model, 'complete')\n    return vectors\n  }\n}\n"],"mappings":"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"}