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

@adhd/sox-embedding-provider

v0.6.4

Published

Pluggable text→vector embedding provider — generic EmbeddingProvider interface, config-driven model resolution, async batch-first API (AsyncIterable). Default: fastembed (local ONNX, >=3 model dims proven). Loud-fail: createEmbeddingProvider() throws Reso

Readme

@adhd/sox-embedding-provider

A pluggable text→vector embedding provider. One EmbeddingProvider interface, one createEmbeddingProvider(config) factory, config-driven model resolution, and an async batch-first API (AsyncIterable<Float32Array>) so a caller gets the first vector back before the last one in a large batch finishes computing. The default provider is fastembed — local ONNX inference, no network calls, no API key. A remote provider is also built in for calling an HTTP embedding endpoint. createEmbeddingProvider() throws a typed ResolutionError synchronously (or as a rejection) if the config is invalid or the model fails to load — it never silently degrades to a different provider.

pnpm add @adhd/sox-embedding-provider

Quick start

import { createEmbeddingProvider } from '@adhd/sox-embedding-provider';

const provider = await createEmbeddingProvider({
  type: 'fastembed',
  model: 'bge-small-en-v1.5',
});

console.log(provider.metadata);
// { modelId: 'bge-small-en-v1.5', dimensions: 384, maxTokens: 512, isRemote: false, isDeterministic: false }

const vec = await provider.embedSingle('The quick brown fox jumps over the lazy dog');
console.log(vec.length); // 384

for await (const v of provider.embedBatch(['first document', 'second document', 'third document'])) {
  console.log(v.length); // 384, one vector per input text, streamed as each is ready
}

API reference

createEmbeddingProvider(config): Promise<EmbeddingProvider>

interface EmbeddingProviderConfig {
  type: string;    // 'fastembed' | 'remote'
  model: string;
  options?: Record<string, unknown>;
  host?: 'shared' | 'private';  // default 'shared' — see "Host posture" below
  idleGraceMs?: number;         // idle window W: a shared host retires W after its last work (default 60000)
}

Throws ResolutionError if type is missing/unknown, or if the model/runtime cannot be loaded. Never throws mid-call afterward — resolution failures happen only at factory time.

EmbeddingProvider

interface EmbeddingProvider {
  readonly metadata: EmbeddingProviderMetadata;
  embedSingle(text: string, role?: EmbedRole): Promise<Float32Array>;
  embedBatch(texts: string[], opts?: { role?: EmbedRole; batchSize?: number }): AsyncIterable<Float32Array>;
  warmUp(texts: string[]): Promise<void>;
  health(): EmbeddingHealth;
}

type EmbedRole = 'document' | 'query';

interface EmbeddingProviderMetadata {
  modelId: string;
  dimensions: number;
  maxTokens: number;
  isRemote: boolean;
  isDeterministic: boolean;
  providerUri?: string;
}

interface EmbeddingHealth {
  configured: string;       // e.g. 'fastembed:bge-base-en-v1.5'
  active: string | null;    // null until warm — never a placeholder model name
  state: 'uninitialized' | 'warming' | 'real' | 'error';
  dimensions: number | null;
  last_error: string | null;
  execution_provider?: string;
}

Every provider advertises its dimensions via metadata — never hardcode a vector dimension for a model; read it from the provider you constructed. warmUp() is currently a no-op on every shipped provider (fastembed and remote both report isDeterministic: false, and warm caching is only meaningful for deterministic providers) — it's safe to call, but don't rely on it to pre-populate a cache today.

fastembed — local ONNX models

const provider = await createEmbeddingProvider({
  type: 'fastembed',
  model: 'bge-m3',              // defaults to 'bge-base-en-v1.5' if omitted
  options: { cacheDir: '/custom/model/cache/path' },  // optional; falls back to $SOX_EMBED_CACHE_DIR, then ~/.cache/sox/models
});

Built-in models (modelId → dimensions):

| modelId | dim | max tokens | notes | |---|---|---|---| | bge-small-en-v1.5 | 384 | 512 | lightweight, ~33M params | | bge-base-en-v1.5 | 768 | 512 | balanced, ~110M params (default) | | multilingual-e5-large | 1024 | 512 | 100+ languages, ~335M params | | bge-m3 | 1024 | 8192 | 100+ languages, long context, ~570M params | | codexembed-400m | 1024 | 8192 | code-only, ~1.6GB RAM, long context |

fastembed inference is routed through one machine-wide, peer-spawned, self-reaping host process per (build, model, execution-provider, cacheDir) — not one ONNX host per consumer process. The build id in the key (a digest of the host module bytes, the Node ABI and the arch) means two builds on one box run two hosts and a consumer never dials a foreign build's host. The first consumer to need it peer-spawns the host through the service-proxy's ensureBackend() singleton spawn-lock; every other consumer on the box dials that same host over a Unix domain socket. The host is compute-only (it holds no store connection) and it is a work-driven drainer (ADR-0022): it retires idleGraceMs after its last completed init/embed/embedBatch once nothing is in flight. Open connections and health probes never keep it alive, and there is no keep-warm. Every request carries {model, cacheDir} and the host loads its own model, so a consumer whose host retired or died is served by a respawned successor transparently (the client re-ensures and retries once). There is no supervised service and no daemon.

Host posture

EmbeddingProviderConfig.host is a typed closed union (default 'shared'), applied process-wide before the accessor is constructed and reported in health().host:

  • 'shared' — funnel through the machine-wide host above. getSharedFastembedProcess() returns a FunneledFastembedClient; its terminate() is a no-op (a consumer must never kill a host other consumers are using).
  • 'private' — the pre-funnel per-process fork (CI/diagnostics). getSharedFastembedProcess() returns the private pool directly.

EmbeddingProviderConfig.idleGraceMs (typed config, default DEFAULT_EMBED_HOST_IDLE_GRACE_MS = 60 s) is the idle window W after the host's last completed work. A host that fails to come up throws a typed TransientEmbeddingError naming the socket — it never silently falls back to a private host.

Lifecycle helpers

import {
  getPrivateFastembedProcess,   // the PRIVATE (un-funneled) per-process pool
  resetSharedFastembedHost,     // heal: ask the live shared host to re-fork its private pool
  getSharedFastembedProcess,    // the host-aware accessor (shared by default)
  FunneledFastembedClient,      // what getSharedFastembedProcess() returns under 'shared'
} from '@adhd/sox-embedding-provider';

const client = getSharedFastembedProcess();  // FunneledFastembedClient under the default
const vec = await client.request({ type: 'embed', text: 'hello' });
console.log(client.started, client.pendingCount, client.hostSocketPath);

resetSharedFastembedHost() is the recovery path for a wedged private child: under 'shared' it asks the live host to tear down and re-fork its private ONNX pool (without killing the host other consumers share); under 'private' the accessor's terminate() path is unchanged.

remote — call an HTTP embedding endpoint

const provider = await createEmbeddingProvider({
  type: 'remote',
  model: 'remote-768',
  options: {
    endpoint: 'https://your-embedding-service.example.com/v1',
    apiKey: process.env.EMBEDDING_API_KEY,
    dimensions: 768,  // optional, default 768
  },
});

options.endpoint is required — createEmbeddingProvider throws ResolutionError without it.

Error taxonomy

class TransientEmbeddingError extends Error { readonly retryAfterMs?: number; }
class PermanentEmbeddingError extends Error {}
class ResolutionError extends Error {}
  • ResolutionError — factory-time only (bad config, model can't load). Never thrown mid-call.
  • TransientEmbeddingError — the caller may retry (e.g. a transient network hiccup on a remote call).
  • PermanentEmbeddingError — the caller must not retry (e.g. empty input text, an invalid API key shape).
import { TransientEmbeddingError, PermanentEmbeddingError } from '@adhd/sox-embedding-provider';

try {
  await provider.embedSingle(text);
} catch (err) {
  if (err instanceof TransientEmbeddingError) {
    // back off and retry, honoring err.retryAfterMs if present
  } else if (err instanceof PermanentEmbeddingError) {
    // do not retry — the input or config itself is the problem
  } else {
    throw err;
  }
}

Invariants

  • createEmbeddingProvider() throws ResolutionError synchronously or as a rejection if the config is invalid or the model/runtime cannot load — never a silent downgrade to another provider.
  • Every provider advertises { modelId, dimensions, isRemote, isDeterministic, providerUri? } via metadata — callers never hardcode dimensions.
  • embedBatch() returns AsyncIterable<Float32Array> — callers receive the first result before the last batch finishes, which matters for sequential local inference.
  • warmUp() is a no-op when isDeterministic === false — currently always true, since every shipped provider hard-codes isDeterministic: false.
  • TransientEmbeddingError → caller may retry; PermanentEmbeddingError → caller must not retry; ResolutionError → factory-time only, never thrown mid-call.