/** * embedders — ready-made {@link Embedder} implementations for the * embedding-backed scorers (memory retrieval, toolChoiceRecorder / scoreMargin). * * The core deliberately ships only `mockEmbedder` (bring-your-own). These are * OPTIONAL and never pulled into the core: each heavy backend is an OPTIONAL * PEER DEPENDENCY, imported LAZILY on first embed, so you install ONLY the one * you use and agentfootprint stays dependency-free. * * openaiEmbedder() — hosted; needs OPENAI_API_KEY; no extra install (fetch). * bedrockEmbedder() — hosted on AWS; Titan Text Embeddings and Cohere Embed * v3 (the model id picks the body shape); credentials * come from the AWS chain, so no key option at all. * peer dep: @aws-sdk/client-bedrock-runtime. * geminiEmbedder() — hosted on Google; Vertex (project + Application * Default Credentials) or the Gemini API (one key), the * same two doors as `gemini()`. Matryoshka sizes, real * task types, and it REFUSES a vector the service admits * it clipped. peer dep: @google/genai. * localEmbedder() — on-device sentence-transformer; no key; offline after a * one-time model fetch. peer dep: @huggingface/transformers. * staticEmbedder() — pure-JS Model2Vec static vectors; no key, no network * (weights bundled). peer dep: @yarflam/potion-base-8m. * * All five satisfy the same `Embedder` shape, so they drop into * `toolChoiceRecorder({ embedder })` / `semanticPipeline({ embedder })` etc. * unchanged. Dimensions differ per model — never mix two in one store. * * ─── Bundlers / browsers: pass `backend` ──────────────────────────────── * * The lazy `import(spec)` above keeps the peer deps optional, but a BUNDLER * cannot see through a variable specifier: the bare name survives into the * output and the browser throws * `TypeError: Failed to resolve module specifier '@huggingface/transformers'` * at first embed. So both on-device factories also accept an ALREADY-IMPORTED * module — a static import your own bundler resolves: * * import * as transformers from '@huggingface/transformers'; * const embedder = localEmbedder({ backend: transformers }); * * Same mechanism as the `client` option on the store adapters (RedisStore, * AgentCoreStore): the library states the surface it needs, the host owns the * construction. Nothing changes for Node callers who pass nothing. * * Not an import path of its own since 9.0.0. This is the implementation barrel * behind `agentfootprint/providers`, which re-exports every name here — same * symbols, one door. Import from the door. */ export type { Embedder } from '../memory/embedding/types.js'; import type { Embedder } from '../memory/embedding/types.js'; import { type GoogleGenAIConnectionOptions } from '../adapters/llm/googleGenAI.js'; export interface OpenAIEmbedderOptions { /** Default: process.env.OPENAI_API_KEY. */ readonly apiKey?: string; /** Default: 'text-embedding-3-small'. */ readonly model?: string; /** * Shorten the vectors the model returns (OpenAI's Matryoshka truncation). * * When set, the value is SENT as the `dimensions` request parameter AND * reported as `.dimensions` — the two can never disagree. Only supported on * `text-embedding-3` and later models; ada-002 rejects it, which is exactly * why nothing is sent unless you ask. * * Leave it unset to get the model's native size (looked up from * {@link NATIVE_DIMENSIONS}). Required for a model this library doesn't know * — see {@link openaiEmbedder}. */ readonly dimensions?: number; /** Override the API base (Azure/OpenAI-compatible gateways). */ readonly baseURL?: string; } /** * OpenAI's hosted embeddings endpoint. * * `.dimensions` is the length callers WILL get back, never an assumption: * an explicit `{ dimensions }` is sent to the API and reported; otherwise the * model's documented native size is reported. A model outside * {@link NATIVE_DIMENSIONS} (a gateway, a self-hosted model behind `baseURL`, * an Azure deployment name, a future OpenAI model) has no size this library can * know, so it is a construction-time error rather than a guess that a vector * store would silently trust. * * `.maxInputChars` (9.1.0) reports **32,000** for the three models above: * their documented 8,191-token window at the stated {@link CHARS_PER_TOKEN} * assumption. An indexer reads it in preference to its own 2,000-character * default, so a corpus split at 2,500 characters is embedded WHOLE here * instead of being clipped by a default measured on an on-device model. * An unknown model declares no ceiling — the indexer's default stands, which * is conservative rather than wrong. * * @throws if there is no API key, or if `model` is unknown and `dimensions` * was not supplied. */ export declare function openaiEmbedder(options?: OpenAIEmbedderOptions): Embedder; /** * The slice of `@aws-sdk/client-bedrock-runtime` {@link bedrockEmbedder} uses. * * Structural, so the real SDK, a pre-built client shared with the rest of your * app, or a test double all satisfy it without this package taking a hard type * dependency on the optional peer. */ export interface BedrockRuntimeLikeClient { /** * `send(command, options?)` — the second argument is where the AWS SDK * takes an `abortSignal`, and it is passed whenever the caller supplied * one, so an aborted indexing run stops paying for embeddings it will * throw away. */ send(command: unknown, options?: { abortSignal?: AbortSignal; }): Promise; } /** The two SDK constructors this embedder needs. */ export interface BedrockRuntimeSdkModule { readonly BedrockRuntimeClient?: new (config: { region?: string; }) => BedrockRuntimeLikeClient; readonly InvokeModelCommand?: new (input: unknown) => unknown; } /** * The request/response SHAPE a Bedrock embedding model speaks (9.3.0). * * `InvokeModel` is one operation with a vendor-specific body on both sides: * Titan takes `{ inputText }` and answers `{ embedding }`, Cohere takes * `{ texts, input_type }` and answers `{ embeddings }`. One model id therefore * does not describe one call, and until 9.3.0 this factory sent Titan's body to * everything — so a Cohere model id was accepted at construction (with * `dimensions`) and failed at the first embed, against the real service, with a * validation error from AWS rather than a sentence from here. */ export type BedrockEmbeddingFamily = 'titan' | 'cohere'; /** * Cohere's `input_type`, which is a real parameter and not a hint: the v3 * models embed a QUERY and a DOCUMENT into deliberately different places, and * the two are meant to be compared with each other. Sending one value for both * halves is a measurable loss of retrieval quality, not a style choice — and * Cohere requires the field, so there is no "unset" to fall back to. */ export type CohereInputType = 'search_document' | 'search_query'; export interface BedrockEmbedderOptions { /** * Bedrock model id. Default `'amazon.titan-embed-text-v2:0'`. * * Four are known by name — Titan V2, Titan V1, and Cohere Embed English / * Multilingual v3 (see {@link BEDROCK_EMBEDDING_MODELS}) — and each brings * its own body shape, vector length and input window. An id that WRAPS one of * those (a cross-region inference profile `us.amazon.titan-embed-text-v2:0`, * or an ARN ending in the model id) is resolved to the model it names. * * Anything else is a model this library has never met: pass `dimensions` * with it (its vector length is not something this can know) and `family` if * it is not Titan-shaped. */ readonly model?: string; /** * Vector length to request. * * Titan V2 is the one CONFIGURABLE model — 1024 (default), 512 or 256 — and * the value is SENT to the model AND reported as `.dimensions`, so the two * can never disagree. Every other known model has ONE size, and asking for a * different one is refused rather than reported: `.dimensions` is what a * vector store fingerprints on, and a wrong one corrupts it silently. * * Required for a model outside {@link BEDROCK_EMBEDDING_MODELS}. */ readonly dimensions?: number; /** * The body shape to speak, when the model id does not say (9.3.0). * * Inferred for every known model and for anything that wraps one, so this is * only for a model id this library has never met — a provisioned-throughput * ARN, a custom deployment. Unknown and unstated, the body is **Titan's**, * which is the shape every release before 9.3.0 sent to everything. * * Stating a family that contradicts a known model id is refused by name. */ readonly family?: BedrockEmbeddingFamily; /** * Pin Cohere's `input_type` instead of deriving it from the call (9.3.0). * * Unset — the default — `embed()` sends `'search_query'` and `embedBatch()` * sends `'search_document'`, because that is what this library's own two * call sites are: retrieval embeds ONE question (`loadRelevant`), indexing * embeds MANY passages (`indexDocuments`, `embedMessages`). Pin it when your * own code uses the two calls differently — embedding a single document, say, * or scoring a batch of queries. * * Ignored by Titan, which has no such parameter. */ readonly inputType?: CohereInputType; /** * The longest input this model reads whole, in CHARACTERS * ({@link Embedder.maxInputChars}). Declared for every known model from its * documented token window; this option is how a model this library does not * know states its own, rather than declaring none and leaving the indexer's * conservative default in place. An explicit value always wins. */ readonly maxInputChars?: number; /** AWS region. Passed to the SDK client when this factory builds one. */ readonly region?: string; /** A pre-built Bedrock runtime client, so one SDK config serves the whole app. */ readonly client?: BedrockRuntimeLikeClient; /** @internal Test injection — skips the SDK require entirely. */ readonly _client?: BedrockRuntimeLikeClient; /** @internal Test injection — the AWS SDK module (exercises the real shim with a mock SDK). */ readonly _sdk?: BedrockRuntimeSdkModule; } /** * Amazon Bedrock's hosted embeddings, through `InvokeModel`. * * No API key option, deliberately: Bedrock authenticates through the AWS * credential chain (environment, profile, instance role, SSO), and inventing * a key parameter would be a second, worse way to configure the same thing. * * ── Why the id carries the DIMENSION COUNT ─────────────────────────────── * `openaiEmbedder` deliberately leaves the size out of its id, because a * store's fingerprint is `'@'` and appends it. This one puts it in, * and the difference is not an inconsistency — it is the one place the * fingerprint cannot reach. * * `MemoryEntry.embeddingModel` stores the id ALONE, and it is the only thing * `SearchOptions.embedderId` filters on: the filter the port describes as * preventing "silent cross-model similarity pollution". Titan V2 at 512 and * Titan V2 at 1024 are different embedding spaces from one model id, so an id * without the size makes those two indistinguishable to that filter — and the * size alone cannot separate them either, since V1 and V2 both answer at * 1024. Only `'bedrock::'` separates all of them. The store's * fingerprint then reads `'bedrock:amazon.titan-embed-text-v2:0:512@512'`, * which restates the size once; a redundant fingerprint is harmless, and a * filter that cannot tell two vector spaces apart is not. * * (Precedent: `localEmbedder` puts `dtype` in its id for the same reason — a * q8 and an fp32 build of one model are near-identical spaces, and "near" is * exactly the difference that surfaces as a mysteriously worse ranking.) * * ── One operation, two body shapes (9.3.0) ─────────────────────────────── * `InvokeModel` is a single API over vendor-specific JSON. Titan takes * `{ inputText }` and answers `{ embedding }`; Cohere takes * `{ texts, input_type }` and answers `{ embeddings }`, embeds up to * {@link COHERE_MAX_TEXTS_PER_CALL} of them per call, and distinguishes a * QUERY from a DOCUMENT. So the model id selects a FAMILY * ({@link BedrockEmbeddingFamily}), and the family owns the request, the * response and the batching. Before this, one shape was sent to everything — * a Cohere id constructed fine and failed at the first embed. * * ── The input ceiling (9.1.0) ──────────────────────────────────────────── * `.maxInputChars` is per MODEL, from its documented token window converted at * the stated {@link CHARS_PER_TOKEN} assumption of 4 characters per token: * **32,000** for both Titan text-embedding models (8,192 tokens — sixteen * times the indexer's own default, which was measured on an on-device model), * and **2,000** for Cohere Embed v3 (512 tokens). Those two numbers are why the * ceiling cannot be a per-vendor constant: the same 2,500-character chunk is * read whole by Titan and truncated by Cohere. Dense text (code, tables, CJK) * tokenises tighter than the assumption; pass an explicit `maxChunkChars` for * such a corpus, and it wins over this number. * * @throws if `model` is unknown and `dimensions` was not supplied; if * `dimensions` is a size the model does not produce; if `family` * contradicts a known model id; or if the SDK is missing and no * `client` / `_client` / `_sdk` was passed. * * @example * ```ts * import { bedrockEmbedder } from 'agentfootprint/providers'; * import { sqliteVectorStore } from 'agentfootprint/memory'; * import { indexFolder } from 'agentfootprint/rag'; * * const embedder = bedrockEmbedder({ region: 'us-east-1', dimensions: 512 }); * await indexFolder('./docs', { to: sqliteVectorStore({ file: './corpus.db' }), embedder }); * ``` * * @example A Cohere embedding model on the same runtime * ```ts * // Body shape, response field, batch size and 512-token window all follow * // from the model id — nothing else changes at the call site. * const embedder = bedrockEmbedder({ model: 'cohere.embed-english-v3' }); * ``` */ export declare function bedrockEmbedder(options?: BedrockEmbedderOptions): Embedder; /** * The slice of `@google/genai` {@link geminiEmbedder} uses — one method. * * Structural, so the real `GoogleGenAI`, a client shared with `gemini()`, or a * `{ models: { embedContent } }` double all satisfy it without this package * taking a hard type dependency on the optional peer. The double is what the * Google pin injects. */ export interface GeminiEmbedClientLike { readonly models: { embedContent(params: GeminiEmbedParams): Promise; }; } /** `EmbedContentParameters`, narrowed to what this adapter sends. */ export interface GeminiEmbedParams { readonly model: string; readonly contents: readonly string[]; readonly config?: { readonly taskType?: string; readonly outputDimensionality?: number; readonly abortSignal?: AbortSignal; }; } /** `EmbedContentResponse`, narrowed to what this adapter reads. */ export interface GeminiEmbedResponse { readonly embeddings?: readonly { readonly values?: readonly number[]; /** * Present on Vertex. `truncated: true` is the service TELLING us it clipped * the input — the one signal that turns the silent half of * {@link Embedder.maxInputChars} into something an adapter can act on. */ readonly statistics?: { readonly truncated?: boolean; readonly tokenCount?: number; }; }[]; } /** * Gemini's `task_type` — a real parameter, not a hint. * * `RETRIEVAL_QUERY` and `RETRIEVAL_DOCUMENT` embed a question and a passage * into deliberately different projections that are MEANT to be compared with * each other, so using one value for both halves is a measurable loss of * retrieval quality. The rest are separate objectives; mixing them in one store * is mixing spaces. */ export type GeminiEmbeddingTaskType = 'RETRIEVAL_QUERY' | 'RETRIEVAL_DOCUMENT' | 'SEMANTIC_SIMILARITY' | 'CLASSIFICATION' | 'CLUSTERING' | 'CODE_RETRIEVAL_QUERY' | 'QUESTION_ANSWERING' | 'FACT_VERIFICATION'; /** What this adapter does when the service says it clipped the input. */ export type GeminiTruncationPolicy = 'refuse' | 'allow'; export interface GeminiEmbedderOptions extends GoogleGenAIConnectionOptions { /** * Embedding model id. Default `'gemini-embedding-001'`. * * Two are known by name (see {@link GEMINI_EMBEDDING_MODELS}); anything else * is a model this library has never met, so pass `dimensions` with it — its * vector length is not something this can know, and a wrong `.dimensions` * corrupts a vector store in silence. */ readonly model?: string; /** * Vector length to request (`outputDimensionality`). * * Both known models are Matryoshka models: they emit 3072 numbers and can be * asked for fewer, and the shorter vector is a genuinely usable embedding * rather than a slice of a longer one. The value is SENT to the model AND * reported as `.dimensions`, so the two can never disagree. Google recommends * 768, 1536 or 3072; any length up to the model's native size is accepted. * * Required for a model outside {@link GEMINI_EMBEDDING_MODELS}. * * Note for stores that rank by DOT PRODUCT or euclidean distance: Google's * shortened vectors are not re-normalised, and Google recommends normalising * them yourself. Nothing in this library needs it — `cosineSimilarity` * divides by both magnitudes — so this adapter returns the model's numbers * unchanged rather than quietly rescaling what you store. */ readonly dimensions?: number; /** * Pin `task_type` instead of deriving it from the call. * * Unset — the default — `embed()` sends `RETRIEVAL_QUERY` and `embedBatch()` * sends `RETRIEVAL_DOCUMENT`, because that is what this library's own two * call sites are: retrieval embeds ONE question (`loadRelevant`), indexing * embeds MANY passages (`indexDocuments`, `embedMessages`). Pin it when your * own code uses the two calls differently, or when the objective is * classification or clustering rather than search. * * Refused by name on a model that does not take the parameter. */ readonly taskType?: GeminiEmbeddingTaskType; /** * What to do when the service reports it CLIPPED the input. Default * `'refuse'`. * * Over-long input is not rejected by Gemini — it is silently truncated, and a * full-looking vector comes back for the opening of the passage. An indexer * then stores the whole chunk as the passage and the clipped vector as its * index, so retrieval cannot find text that is visibly present in the passage * it later serves. Nothing throws, nothing scores zero; the corpus is quietly, * partially indexed. * * `.maxInputChars` exists to stop that BEFORE the call, and it is an * assumption (see {@link CHARS_PER_TOKEN}) — dense text tokenises tighter. * `statistics.truncated` is the service saying it happened anyway, and this * adapter is the only thing that sees it. `'refuse'` turns it into an error * naming the fix; `'allow'` returns the clipped vector, which is what every * library that does not look at the field already does. * * Detection depends on the service RETURNING `statistics` — Vertex does. When * it is absent this adapter cannot tell, and says so here rather than * implying a guarantee. */ readonly onTruncation?: GeminiTruncationPolicy; /** * The longest input this model reads whole, in CHARACTERS * ({@link Embedder.maxInputChars}). Declared for every known model from its * documented token window; this option is how a model this library does not * know states its own. An explicit value always wins. */ readonly maxInputChars?: number; /** @internal Test injection — skips the SDK require entirely. */ readonly _client?: GeminiEmbedClientLike; } /** * Google's hosted embeddings, through `models.embedContent` — on Vertex or on * the Gemini API. * * The door is chosen exactly as `gemini()` chooses it: `{ project, location }` * is Vertex with Application Default Credentials, `{ apiKey }` is the Gemini * API, and neither is guessed. One `GoogleGenAI` client serves both this and * the LLM provider, so an app that talks to both can build the client once and * pass it as `_client`. * * ── Why the id carries the DIMENSION COUNT ─────────────────────────────── * `'gemini::'`, for the reason `bedrockEmbedder` gives at length: * `MemoryEntry.embeddingModel` stores the id ALONE and it is the only thing * `SearchOptions.embedderId` filters on. One model id at 768 and the same model * id at 3072 are different embedding spaces, so an id without the size cannot * tell them apart — and neither can the size alone, since both known models are * 3072 natively. * * The `taskType` is deliberately NOT in the id, matching `bedrockEmbedder`'s * treatment of Cohere's `input_type`: the default query/document pair is ONE * space by construction (the two projections exist to be compared with each * other), and a pinned value pins both call sites at once, so the store stays * self-consistent either way. * * ── The input ceiling, and the service telling on itself (9.1.0) ───────── * `.maxInputChars` is per MODEL, from its documented token window at the stated * {@link CHARS_PER_TOKEN} assumption: **8,000** for `gemini-embedding-001` * (2,048 tokens) and **32,000** for `gemini-embedding-2` (8,192). A quarter of * the newer model's window is not a rounding difference — it is the whole * reason the ceiling is per-model. Past the ceiling Gemini CLIPS rather than * refuses, so `onTruncation` decides what happens when the response admits it. * * @throws if `model` is unknown and `dimensions` was not supplied; if * `dimensions` exceeds what the model can produce; if `taskType` is set * on a model that takes none; if neither a project nor an API key is * resolvable; or if `@google/genai` is not installed and no `_client` * was passed. * * @example * ```ts * import { geminiEmbedder } from 'agentfootprint/providers'; * import { sqliteVectorStore } from 'agentfootprint/memory'; * import { indexFolder } from 'agentfootprint/rag'; * * const embedder = geminiEmbedder({ project: 'my-project', dimensions: 768 }); * await indexFolder('./docs', { to: sqliteVectorStore({ file: './corpus.db' }), embedder }); * ``` * * @example The Gemini API door, and a store that must never hold a clipped vector * ```ts * const embedder = geminiEmbedder({ * apiKey: process.env.GEMINI_API_KEY, * onTruncation: 'refuse', // the default — stated here because it is the point * }); * ``` */ export declare function geminiEmbedder(options?: GeminiEmbedderOptions): Embedder; /** * The slice of `@huggingface/transformers` {@link localEmbedder} uses. * * Structural, so `await import('@huggingface/transformers')` (or a stub, or a * pinned fork) satisfies it without this package taking a hard type dependency * on the optional peer. */ export interface TransformersBackend { /** transformers.js `pipeline(task, model, options)`. */ pipeline(task: string, model?: string, options?: Record): Promise; /** transformers.js `env` — mutated only when `cacheDir` is set. */ env?: unknown; } export interface LocalEmbedderOptions { /** ONNX model id. Default 'Xenova/all-MiniLM-L6-v2' (384-dim). */ readonly model?: string; /** Vector length of the model. Default 384. */ readonly dimensions?: number; /** Quantization. Default 'q8' (smallest); use 'fp32' for max fidelity. */ readonly dtype?: string; /** On-disk model cache directory. */ readonly cacheDir?: string; /** * Longest input this model reads whole, in characters (9.1.0). Default * {@link LOCAL_MAX_INPUT_CHARS} — the MEASURED cliff of the default model. * * The option exists because `model` is swappable and the cliff belongs to * the MODEL, not to this factory. A long-context embedding model (one of the * 8k-token sentence-transformer builds) reads far more than the default * says, and without a way to state that, an indexer would keep cutting its * corpus into pieces a quarter the size the model can take. Nothing verifies * it — it is your model's documented window, declared for the indexer to * read. */ readonly maxInputChars?: number; /** * An ALREADY-IMPORTED `@huggingface/transformers`. Supply this and the lazy * `import('@huggingface/transformers')` never happens — which is what makes * the embedder work in a BUNDLED app, where a bare specifier reaches the * browser unresolved: * * import * as transformers from '@huggingface/transformers'; * localEmbedder({ backend: transformers }); * * Your bundler resolves that static import; the peer dep stays optional for * everyone who doesn't. */ readonly backend?: TransformersBackend; } export declare function localEmbedder(options?: LocalEmbedderOptions): Embedder; /** * The slice of a Model2Vec package {@link staticEmbedder} uses: a batch * `embed`/`encode`, on the module or on its default export. * * Structural, so `await import('@yarflam/potion-base-8m')` — or any other * Model2Vec build with one of those shapes — satisfies it. */ export interface Model2VecBackend { /** Batch embed: `embed(texts) => vectors` (may be async). */ embed?(texts: readonly string[]): unknown; /** Alternative name some builds use. */ encode?(texts: readonly string[]): unknown; /** A default export that is the fn, or carries `embed`/`encode`. */ readonly default?: unknown; } export interface StaticEmbedderOptions { /** Vector length of the bundled model. Default 256 (potion-base-8m). */ readonly dimensions?: number; /** Override the package specifier for a different Model2Vec build. */ readonly module?: string; /** * An ALREADY-IMPORTED Model2Vec module. Supply this and no dynamic import * happens — the only way this embedder can run in a BUNDLED app, since a * bundler cannot resolve the specifier `module` names: * * import * as potion from '@yarflam/potion-base-8m'; * staticEmbedder({ backend: potion }); * * Takes precedence over `module`. (The potion backend itself is Node-only * today — see the embedders guide.) */ readonly backend?: Model2VecBackend; } export declare function staticEmbedder(options?: StaticEmbedderOptions): Embedder;