agentfootprint 9.12.0 → 9.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/llm/GeminiProvider.js +519 -0
- package/dist/adapters/llm/GeminiProvider.js.map +1 -0
- package/dist/adapters/llm/contextWindow.js +20 -0
- package/dist/adapters/llm/contextWindow.js.map +1 -1
- package/dist/adapters/llm/createProvider.js +3 -0
- package/dist/adapters/llm/createProvider.js.map +1 -1
- package/dist/adapters/llm/googleGenAI.js +109 -0
- package/dist/adapters/llm/googleGenAI.js.map +1 -0
- package/dist/embedders/index.js +233 -1
- package/dist/embedders/index.js.map +1 -1
- package/dist/esm/adapters/llm/GeminiProvider.d.ts +204 -0
- package/dist/esm/adapters/llm/GeminiProvider.js +514 -0
- package/dist/esm/adapters/llm/GeminiProvider.js.map +1 -0
- package/dist/esm/adapters/llm/contextWindow.js +20 -0
- package/dist/esm/adapters/llm/contextWindow.js.map +1 -1
- package/dist/esm/adapters/llm/createProvider.d.ts +4 -1
- package/dist/esm/adapters/llm/createProvider.js +3 -0
- package/dist/esm/adapters/llm/createProvider.js.map +1 -1
- package/dist/esm/adapters/llm/googleGenAI.d.ts +67 -0
- package/dist/esm/adapters/llm/googleGenAI.js +105 -0
- package/dist/esm/adapters/llm/googleGenAI.js.map +1 -0
- package/dist/esm/embedders/index.d.ts +188 -1
- package/dist/esm/embedders/index.js +231 -0
- package/dist/esm/embedders/index.js.map +1 -1
- package/dist/esm/providers.d.ts +3 -0
- package/dist/esm/providers.js +2 -0
- package/dist/esm/providers.js.map +1 -1
- package/dist/providers.js +5 -1
- package/dist/providers.js.map +1 -1
- package/dist/types/adapters/llm/GeminiProvider.d.ts +205 -0
- package/dist/types/adapters/llm/GeminiProvider.d.ts.map +1 -0
- package/dist/types/adapters/llm/contextWindow.d.ts.map +1 -1
- package/dist/types/adapters/llm/createProvider.d.ts +4 -1
- package/dist/types/adapters/llm/createProvider.d.ts.map +1 -1
- package/dist/types/adapters/llm/googleGenAI.d.ts +68 -0
- package/dist/types/adapters/llm/googleGenAI.d.ts.map +1 -0
- package/dist/types/embedders/index.d.ts +188 -1
- package/dist/types/embedders/index.d.ts.map +1 -1
- package/dist/types/providers.d.ts +3 -0
- package/dist/types/providers.d.ts.map +1 -1
- package/package.json +7 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createProvider.js","sourceRoot":"","sources":["../../../../src/adapters/llm/createProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,IAAI,EAA4B,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AAClF,OAAO,EAAE,MAAM,EAAE,WAAW,EAA8B,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,MAAM,EAA8B,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,OAAO,EAA+B,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EACL,gBAAgB,GAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAqC,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"createProvider.js","sourceRoot":"","sources":["../../../../src/adapters/llm/createProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* googleGenAI — the one place a `GoogleGenAI` client gets built (9.13.0).
|
|
3
|
+
*
|
|
4
|
+
* Two adapters reach for the same SDK client — `gemini()` (the LLM provider)
|
|
5
|
+
* and `geminiEmbedder()` — and they need the SAME two doors, the SAME refusals
|
|
6
|
+
* and the SAME `_client` seam. Duplicating that is how the two drift: one grows
|
|
7
|
+
* a fix, the other keeps the bug, and the difference is invisible until a
|
|
8
|
+
* consumer hits the door that was not fixed.
|
|
9
|
+
*
|
|
10
|
+
* So the connection lives here and the adapters own only their own calls.
|
|
11
|
+
* Nothing in this file knows what a model, a message or a vector is.
|
|
12
|
+
*
|
|
13
|
+
* ─── The two doors ──────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* • **Vertex** — `{ project, location }` → `new GoogleGenAI({ vertexai: true,
|
|
16
|
+
* project, location, googleAuthOptions })`. Credentials come from
|
|
17
|
+
* Application Default Credentials: `GOOGLE_APPLICATION_CREDENTIALS`, the
|
|
18
|
+
* gcloud user credentials, or the metadata server on GCE / Cloud Run. There
|
|
19
|
+
* is no API key on this path.
|
|
20
|
+
* • **Gemini API (AI Studio)** — `{ apiKey }` → `new GoogleGenAI({ apiKey })`.
|
|
21
|
+
* One key, no cloud project.
|
|
22
|
+
*
|
|
23
|
+
* ─── The API version, which is not what you would guess ─────────────
|
|
24
|
+
*
|
|
25
|
+
* The SDK's DEFAULT API version is `v1beta1` on Vertex and `v1beta` on the
|
|
26
|
+
* Gemini API — verified against @google/genai 2.16.0 and pinned by
|
|
27
|
+
* `test/adapters/google/google-surface-pin.test.ts`. It is not `v1`. Callers
|
|
28
|
+
* who need a specific version pass `apiVersion`; callers who do not at least
|
|
29
|
+
* find the truth stated instead of assuming a GA path.
|
|
30
|
+
*/
|
|
31
|
+
/** The connection half of a Google adapter's options — the same on both. */
|
|
32
|
+
export interface GoogleGenAIConnectionOptions {
|
|
33
|
+
/** Google Cloud project id — the VERTEX door. Env fallback (read by the SDK):
|
|
34
|
+
* `GOOGLE_CLOUD_PROJECT`. */
|
|
35
|
+
readonly project?: string;
|
|
36
|
+
/** Vertex location, e.g. `'us-central1'` or `'global'` (the SDK's default).
|
|
37
|
+
* Env fallback: `GOOGLE_CLOUD_LOCATION`. */
|
|
38
|
+
readonly location?: string;
|
|
39
|
+
/** API key — the GEMINI API door. Env fallbacks: `GEMINI_API_KEY`, then
|
|
40
|
+
* `GOOGLE_API_KEY`. */
|
|
41
|
+
readonly apiKey?: string;
|
|
42
|
+
/** Force the door instead of inferring it: `true` = Vertex, `false` = the
|
|
43
|
+
* Gemini API. */
|
|
44
|
+
readonly vertexai?: boolean;
|
|
45
|
+
/** Pin the API version. Unset means the SDK's default — `v1beta1` on Vertex,
|
|
46
|
+
* `v1beta` on the Gemini API. */
|
|
47
|
+
readonly apiVersion?: string;
|
|
48
|
+
/** `GoogleAuthOptions` for the Vertex door — a key file, scopes, or an
|
|
49
|
+
* `AuthClient` you built (workload identity federation, impersonation). */
|
|
50
|
+
readonly googleAuthOptions?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build (or accept) a `GoogleGenAI` client.
|
|
54
|
+
*
|
|
55
|
+
* `injected` is the `_client` test seam: a structural double exposing only the
|
|
56
|
+
* namespace members the calling adapter uses. When it is present the SDK is
|
|
57
|
+
* never required, no environment is read and no refusal fires — a test drives
|
|
58
|
+
* the adapter's translation with no Google anywhere near it.
|
|
59
|
+
*
|
|
60
|
+
* @param factory the public name to put in refusals — `'gemini'`,
|
|
61
|
+
* `'geminiEmbedder'`. A message that names the wrong factory
|
|
62
|
+
* sends the reader to the wrong line of their own code.
|
|
63
|
+
* @throws when neither a project nor an API key is resolvable; when
|
|
64
|
+
* `vertexai: true` is asked for without a project; and when
|
|
65
|
+
* `@google/genai` is not installed.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveGoogleGenAIClient<T>(options: GoogleGenAIConnectionOptions, factory: string, injected?: T): T;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* googleGenAI — the one place a `GoogleGenAI` client gets built (9.13.0).
|
|
3
|
+
*
|
|
4
|
+
* Two adapters reach for the same SDK client — `gemini()` (the LLM provider)
|
|
5
|
+
* and `geminiEmbedder()` — and they need the SAME two doors, the SAME refusals
|
|
6
|
+
* and the SAME `_client` seam. Duplicating that is how the two drift: one grows
|
|
7
|
+
* a fix, the other keeps the bug, and the difference is invisible until a
|
|
8
|
+
* consumer hits the door that was not fixed.
|
|
9
|
+
*
|
|
10
|
+
* So the connection lives here and the adapters own only their own calls.
|
|
11
|
+
* Nothing in this file knows what a model, a message or a vector is.
|
|
12
|
+
*
|
|
13
|
+
* ─── The two doors ──────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* • **Vertex** — `{ project, location }` → `new GoogleGenAI({ vertexai: true,
|
|
16
|
+
* project, location, googleAuthOptions })`. Credentials come from
|
|
17
|
+
* Application Default Credentials: `GOOGLE_APPLICATION_CREDENTIALS`, the
|
|
18
|
+
* gcloud user credentials, or the metadata server on GCE / Cloud Run. There
|
|
19
|
+
* is no API key on this path.
|
|
20
|
+
* • **Gemini API (AI Studio)** — `{ apiKey }` → `new GoogleGenAI({ apiKey })`.
|
|
21
|
+
* One key, no cloud project.
|
|
22
|
+
*
|
|
23
|
+
* ─── The API version, which is not what you would guess ─────────────
|
|
24
|
+
*
|
|
25
|
+
* The SDK's DEFAULT API version is `v1beta1` on Vertex and `v1beta` on the
|
|
26
|
+
* Gemini API — verified against @google/genai 2.16.0 and pinned by
|
|
27
|
+
* `test/adapters/google/google-surface-pin.test.ts`. It is not `v1`. Callers
|
|
28
|
+
* who need a specific version pass `apiVersion`; callers who do not at least
|
|
29
|
+
* find the truth stated instead of assuming a GA path.
|
|
30
|
+
*/
|
|
31
|
+
import { lazyRequire } from '../../lib/lazyRequire.js';
|
|
32
|
+
/**
|
|
33
|
+
* Build (or accept) a `GoogleGenAI` client.
|
|
34
|
+
*
|
|
35
|
+
* `injected` is the `_client` test seam: a structural double exposing only the
|
|
36
|
+
* namespace members the calling adapter uses. When it is present the SDK is
|
|
37
|
+
* never required, no environment is read and no refusal fires — a test drives
|
|
38
|
+
* the adapter's translation with no Google anywhere near it.
|
|
39
|
+
*
|
|
40
|
+
* @param factory the public name to put in refusals — `'gemini'`,
|
|
41
|
+
* `'geminiEmbedder'`. A message that names the wrong factory
|
|
42
|
+
* sends the reader to the wrong line of their own code.
|
|
43
|
+
* @throws when neither a project nor an API key is resolvable; when
|
|
44
|
+
* `vertexai: true` is asked for without a project; and when
|
|
45
|
+
* `@google/genai` is not installed.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveGoogleGenAIClient(options, factory, injected) {
|
|
48
|
+
if (injected)
|
|
49
|
+
return injected;
|
|
50
|
+
const env = (typeof process !== 'undefined' ? process.env : {});
|
|
51
|
+
// Read HERE only to decide whether there is enough configuration to build
|
|
52
|
+
// anything at all. The values are never copied into the constructor, so the
|
|
53
|
+
// SDK stays the single resolver of its own environment variables — one place
|
|
54
|
+
// that answers "where did my project come from", not two that can disagree.
|
|
55
|
+
// An EMPTY variable is not a setting. `GOOGLE_CLOUD_PROJECT=` in a shell or a
|
|
56
|
+
// CI matrix is how a project comes to be "present" and unusable — the guards
|
|
57
|
+
// below would pass and the SDK would fail on the first call instead.
|
|
58
|
+
const set = (value) => value !== undefined && value.trim().length > 0 ? value : undefined;
|
|
59
|
+
const project = set(options.project) ?? set(env.GOOGLE_CLOUD_PROJECT);
|
|
60
|
+
const apiKey = set(options.apiKey) ?? set(env.GEMINI_API_KEY) ?? set(env.GOOGLE_API_KEY);
|
|
61
|
+
const vertexai = options.vertexai ?? (project !== undefined ? true : undefined);
|
|
62
|
+
if (vertexai !== false && project === undefined && apiKey === undefined) {
|
|
63
|
+
throw new Error(`${factory}: no Google project and no API key — this factory cannot tell which service ` +
|
|
64
|
+
'you meant, and the SDK would warn on stderr, construct anyway, and fail on the first ' +
|
|
65
|
+
'call with something that reads like a network problem.\n' +
|
|
66
|
+
` Vertex: ${factory}({ project: "my-project", location: "us-central1" }) — or set ` +
|
|
67
|
+
'GOOGLE_CLOUD_PROJECT; credentials come from Application Default Credentials.\n' +
|
|
68
|
+
` Gemini API: ${factory}({ apiKey: process.env.GEMINI_API_KEY }) — or set ` +
|
|
69
|
+
'GEMINI_API_KEY.\n' +
|
|
70
|
+
` Tests: ${factory}({ _client }) with a { models: { … } } double — no SDK, no ` +
|
|
71
|
+
'network, no credentials.');
|
|
72
|
+
}
|
|
73
|
+
if (vertexai === true && project === undefined) {
|
|
74
|
+
throw new Error(`${factory}: \`vertexai: true\` selects Vertex, which is addressed by PROJECT and not by ` +
|
|
75
|
+
'key. Pass `{ project }` (or set GOOGLE_CLOUD_PROJECT), or drop `vertexai` and pass ' +
|
|
76
|
+
'`{ apiKey }` to use the Gemini API instead.');
|
|
77
|
+
}
|
|
78
|
+
let GoogleGenAI;
|
|
79
|
+
try {
|
|
80
|
+
const mod = lazyRequire('@google/genai');
|
|
81
|
+
GoogleGenAI = (mod.GoogleGenAI ?? mod.default?.GoogleGenAI);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error(`${factory} requires the \`@google/genai\` package.\n` +
|
|
85
|
+
' Install: npm install @google/genai\n' +
|
|
86
|
+
' Or pass `_client` for test injection.');
|
|
87
|
+
}
|
|
88
|
+
if (!GoogleGenAI) {
|
|
89
|
+
throw new Error(`${factory}: \`@google/genai\` is installed but exports no \`GoogleGenAI\`. These adapters ` +
|
|
90
|
+
'are built against @google/genai 2.x — update the package.');
|
|
91
|
+
}
|
|
92
|
+
// Only what was ASKED for goes on the wire; everything omitted is left to the
|
|
93
|
+
// SDK's own environment resolution.
|
|
94
|
+
return new GoogleGenAI({
|
|
95
|
+
...(vertexai !== undefined && { vertexai }),
|
|
96
|
+
...(options.project !== undefined && { project: options.project }),
|
|
97
|
+
...(options.location !== undefined && { location: options.location }),
|
|
98
|
+
...(options.apiKey !== undefined && { apiKey: options.apiKey }),
|
|
99
|
+
...(options.apiVersion !== undefined && { apiVersion: options.apiVersion }),
|
|
100
|
+
...(options.googleAuthOptions !== undefined && {
|
|
101
|
+
googleAuthOptions: options.googleAuthOptions,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=googleGenAI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"googleGenAI.js","sourceRoot":"","sources":["../../../../src/adapters/llm/googleGenAI.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
* v3 (the model id picks the body shape); credentials
|
|
13
13
|
* come from the AWS chain, so no key option at all.
|
|
14
14
|
* peer dep: @aws-sdk/client-bedrock-runtime.
|
|
15
|
+
* geminiEmbedder() — hosted on Google; Vertex (project + Application
|
|
16
|
+
* Default Credentials) or the Gemini API (one key), the
|
|
17
|
+
* same two doors as `gemini()`. Matryoshka sizes, real
|
|
18
|
+
* task types, and it REFUSES a vector the service admits
|
|
19
|
+
* it clipped. peer dep: @google/genai.
|
|
15
20
|
* localEmbedder() — on-device sentence-transformer; no key; offline after a
|
|
16
21
|
* one-time model fetch. peer dep: @huggingface/transformers.
|
|
17
22
|
* staticEmbedder() — pure-JS Model2Vec static vectors; no key, no network
|
|
18
23
|
* (weights bundled). peer dep: @yarflam/potion-base-8m.
|
|
19
24
|
*
|
|
20
|
-
* All
|
|
25
|
+
* All five satisfy the same `Embedder` shape, so they drop into
|
|
21
26
|
* `toolChoiceRecorder({ embedder })` / `semanticPipeline({ embedder })` etc.
|
|
22
27
|
* unchanged. Dimensions differ per model — never mix two in one store.
|
|
23
28
|
*
|
|
@@ -43,6 +48,7 @@
|
|
|
43
48
|
*/
|
|
44
49
|
export type { Embedder } from '../memory/embedding/types.js';
|
|
45
50
|
import type { Embedder } from '../memory/embedding/types.js';
|
|
51
|
+
import { type GoogleGenAIConnectionOptions } from '../adapters/llm/googleGenAI.js';
|
|
46
52
|
export interface OpenAIEmbedderOptions {
|
|
47
53
|
/** Default: process.env.OPENAI_API_KEY. */
|
|
48
54
|
readonly apiKey?: string;
|
|
@@ -272,6 +278,187 @@ export interface BedrockEmbedderOptions {
|
|
|
272
278
|
* ```
|
|
273
279
|
*/
|
|
274
280
|
export declare function bedrockEmbedder(options?: BedrockEmbedderOptions): Embedder;
|
|
281
|
+
/**
|
|
282
|
+
* The slice of `@google/genai` {@link geminiEmbedder} uses — one method.
|
|
283
|
+
*
|
|
284
|
+
* Structural, so the real `GoogleGenAI`, a client shared with `gemini()`, or a
|
|
285
|
+
* `{ models: { embedContent } }` double all satisfy it without this package
|
|
286
|
+
* taking a hard type dependency on the optional peer. The double is what the
|
|
287
|
+
* Google pin injects.
|
|
288
|
+
*/
|
|
289
|
+
export interface GeminiEmbedClientLike {
|
|
290
|
+
readonly models: {
|
|
291
|
+
embedContent(params: GeminiEmbedParams): Promise<GeminiEmbedResponse>;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** `EmbedContentParameters`, narrowed to what this adapter sends. */
|
|
295
|
+
export interface GeminiEmbedParams {
|
|
296
|
+
readonly model: string;
|
|
297
|
+
readonly contents: readonly string[];
|
|
298
|
+
readonly config?: {
|
|
299
|
+
readonly taskType?: string;
|
|
300
|
+
readonly outputDimensionality?: number;
|
|
301
|
+
readonly abortSignal?: AbortSignal;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** `EmbedContentResponse`, narrowed to what this adapter reads. */
|
|
305
|
+
export interface GeminiEmbedResponse {
|
|
306
|
+
readonly embeddings?: readonly {
|
|
307
|
+
readonly values?: readonly number[];
|
|
308
|
+
/**
|
|
309
|
+
* Present on Vertex. `truncated: true` is the service TELLING us it clipped
|
|
310
|
+
* the input — the one signal that turns the silent half of
|
|
311
|
+
* {@link Embedder.maxInputChars} into something an adapter can act on.
|
|
312
|
+
*/
|
|
313
|
+
readonly statistics?: {
|
|
314
|
+
readonly truncated?: boolean;
|
|
315
|
+
readonly tokenCount?: number;
|
|
316
|
+
};
|
|
317
|
+
}[];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Gemini's `task_type` — a real parameter, not a hint.
|
|
321
|
+
*
|
|
322
|
+
* `RETRIEVAL_QUERY` and `RETRIEVAL_DOCUMENT` embed a question and a passage
|
|
323
|
+
* into deliberately different projections that are MEANT to be compared with
|
|
324
|
+
* each other, so using one value for both halves is a measurable loss of
|
|
325
|
+
* retrieval quality. The rest are separate objectives; mixing them in one store
|
|
326
|
+
* is mixing spaces.
|
|
327
|
+
*/
|
|
328
|
+
export type GeminiEmbeddingTaskType = 'RETRIEVAL_QUERY' | 'RETRIEVAL_DOCUMENT' | 'SEMANTIC_SIMILARITY' | 'CLASSIFICATION' | 'CLUSTERING' | 'CODE_RETRIEVAL_QUERY' | 'QUESTION_ANSWERING' | 'FACT_VERIFICATION';
|
|
329
|
+
/** What this adapter does when the service says it clipped the input. */
|
|
330
|
+
export type GeminiTruncationPolicy = 'refuse' | 'allow';
|
|
331
|
+
export interface GeminiEmbedderOptions extends GoogleGenAIConnectionOptions {
|
|
332
|
+
/**
|
|
333
|
+
* Embedding model id. Default `'gemini-embedding-001'`.
|
|
334
|
+
*
|
|
335
|
+
* Two are known by name (see {@link GEMINI_EMBEDDING_MODELS}); anything else
|
|
336
|
+
* is a model this library has never met, so pass `dimensions` with it — its
|
|
337
|
+
* vector length is not something this can know, and a wrong `.dimensions`
|
|
338
|
+
* corrupts a vector store in silence.
|
|
339
|
+
*/
|
|
340
|
+
readonly model?: string;
|
|
341
|
+
/**
|
|
342
|
+
* Vector length to request (`outputDimensionality`).
|
|
343
|
+
*
|
|
344
|
+
* Both known models are Matryoshka models: they emit 3072 numbers and can be
|
|
345
|
+
* asked for fewer, and the shorter vector is a genuinely usable embedding
|
|
346
|
+
* rather than a slice of a longer one. The value is SENT to the model AND
|
|
347
|
+
* reported as `.dimensions`, so the two can never disagree. Google recommends
|
|
348
|
+
* 768, 1536 or 3072; any length up to the model's native size is accepted.
|
|
349
|
+
*
|
|
350
|
+
* Required for a model outside {@link GEMINI_EMBEDDING_MODELS}.
|
|
351
|
+
*
|
|
352
|
+
* Note for stores that rank by DOT PRODUCT or euclidean distance: Google's
|
|
353
|
+
* shortened vectors are not re-normalised, and Google recommends normalising
|
|
354
|
+
* them yourself. Nothing in this library needs it — `cosineSimilarity`
|
|
355
|
+
* divides by both magnitudes — so this adapter returns the model's numbers
|
|
356
|
+
* unchanged rather than quietly rescaling what you store.
|
|
357
|
+
*/
|
|
358
|
+
readonly dimensions?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Pin `task_type` instead of deriving it from the call.
|
|
361
|
+
*
|
|
362
|
+
* Unset — the default — `embed()` sends `RETRIEVAL_QUERY` and `embedBatch()`
|
|
363
|
+
* sends `RETRIEVAL_DOCUMENT`, because that is what this library's own two
|
|
364
|
+
* call sites are: retrieval embeds ONE question (`loadRelevant`), indexing
|
|
365
|
+
* embeds MANY passages (`indexDocuments`, `embedMessages`). Pin it when your
|
|
366
|
+
* own code uses the two calls differently, or when the objective is
|
|
367
|
+
* classification or clustering rather than search.
|
|
368
|
+
*
|
|
369
|
+
* Refused by name on a model that does not take the parameter.
|
|
370
|
+
*/
|
|
371
|
+
readonly taskType?: GeminiEmbeddingTaskType;
|
|
372
|
+
/**
|
|
373
|
+
* What to do when the service reports it CLIPPED the input. Default
|
|
374
|
+
* `'refuse'`.
|
|
375
|
+
*
|
|
376
|
+
* Over-long input is not rejected by Gemini — it is silently truncated, and a
|
|
377
|
+
* full-looking vector comes back for the opening of the passage. An indexer
|
|
378
|
+
* then stores the whole chunk as the passage and the clipped vector as its
|
|
379
|
+
* index, so retrieval cannot find text that is visibly present in the passage
|
|
380
|
+
* it later serves. Nothing throws, nothing scores zero; the corpus is quietly,
|
|
381
|
+
* partially indexed.
|
|
382
|
+
*
|
|
383
|
+
* `.maxInputChars` exists to stop that BEFORE the call, and it is an
|
|
384
|
+
* assumption (see {@link CHARS_PER_TOKEN}) — dense text tokenises tighter.
|
|
385
|
+
* `statistics.truncated` is the service saying it happened anyway, and this
|
|
386
|
+
* adapter is the only thing that sees it. `'refuse'` turns it into an error
|
|
387
|
+
* naming the fix; `'allow'` returns the clipped vector, which is what every
|
|
388
|
+
* library that does not look at the field already does.
|
|
389
|
+
*
|
|
390
|
+
* Detection depends on the service RETURNING `statistics` — Vertex does. When
|
|
391
|
+
* it is absent this adapter cannot tell, and says so here rather than
|
|
392
|
+
* implying a guarantee.
|
|
393
|
+
*/
|
|
394
|
+
readonly onTruncation?: GeminiTruncationPolicy;
|
|
395
|
+
/**
|
|
396
|
+
* The longest input this model reads whole, in CHARACTERS
|
|
397
|
+
* ({@link Embedder.maxInputChars}). Declared for every known model from its
|
|
398
|
+
* documented token window; this option is how a model this library does not
|
|
399
|
+
* know states its own. An explicit value always wins.
|
|
400
|
+
*/
|
|
401
|
+
readonly maxInputChars?: number;
|
|
402
|
+
/** @internal Test injection — skips the SDK require entirely. */
|
|
403
|
+
readonly _client?: GeminiEmbedClientLike;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Google's hosted embeddings, through `models.embedContent` — on Vertex or on
|
|
407
|
+
* the Gemini API.
|
|
408
|
+
*
|
|
409
|
+
* The door is chosen exactly as `gemini()` chooses it: `{ project, location }`
|
|
410
|
+
* is Vertex with Application Default Credentials, `{ apiKey }` is the Gemini
|
|
411
|
+
* API, and neither is guessed. One `GoogleGenAI` client serves both this and
|
|
412
|
+
* the LLM provider, so an app that talks to both can build the client once and
|
|
413
|
+
* pass it as `_client`.
|
|
414
|
+
*
|
|
415
|
+
* ── Why the id carries the DIMENSION COUNT ───────────────────────────────
|
|
416
|
+
* `'gemini:<model>:<dims>'`, for the reason `bedrockEmbedder` gives at length:
|
|
417
|
+
* `MemoryEntry.embeddingModel` stores the id ALONE and it is the only thing
|
|
418
|
+
* `SearchOptions.embedderId` filters on. One model id at 768 and the same model
|
|
419
|
+
* id at 3072 are different embedding spaces, so an id without the size cannot
|
|
420
|
+
* tell them apart — and neither can the size alone, since both known models are
|
|
421
|
+
* 3072 natively.
|
|
422
|
+
*
|
|
423
|
+
* The `taskType` is deliberately NOT in the id, matching `bedrockEmbedder`'s
|
|
424
|
+
* treatment of Cohere's `input_type`: the default query/document pair is ONE
|
|
425
|
+
* space by construction (the two projections exist to be compared with each
|
|
426
|
+
* other), and a pinned value pins both call sites at once, so the store stays
|
|
427
|
+
* self-consistent either way.
|
|
428
|
+
*
|
|
429
|
+
* ── The input ceiling, and the service telling on itself (9.1.0) ─────────
|
|
430
|
+
* `.maxInputChars` is per MODEL, from its documented token window at the stated
|
|
431
|
+
* {@link CHARS_PER_TOKEN} assumption: **8,000** for `gemini-embedding-001`
|
|
432
|
+
* (2,048 tokens) and **32,000** for `gemini-embedding-2` (8,192). A quarter of
|
|
433
|
+
* the newer model's window is not a rounding difference — it is the whole
|
|
434
|
+
* reason the ceiling is per-model. Past the ceiling Gemini CLIPS rather than
|
|
435
|
+
* refuses, so `onTruncation` decides what happens when the response admits it.
|
|
436
|
+
*
|
|
437
|
+
* @throws if `model` is unknown and `dimensions` was not supplied; if
|
|
438
|
+
* `dimensions` exceeds what the model can produce; if `taskType` is set
|
|
439
|
+
* on a model that takes none; if neither a project nor an API key is
|
|
440
|
+
* resolvable; or if `@google/genai` is not installed and no `_client`
|
|
441
|
+
* was passed.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```ts
|
|
445
|
+
* import { geminiEmbedder } from 'agentfootprint/providers';
|
|
446
|
+
* import { sqliteVectorStore } from 'agentfootprint/memory';
|
|
447
|
+
* import { indexFolder } from 'agentfootprint/rag';
|
|
448
|
+
*
|
|
449
|
+
* const embedder = geminiEmbedder({ project: 'my-project', dimensions: 768 });
|
|
450
|
+
* await indexFolder('./docs', { to: sqliteVectorStore({ file: './corpus.db' }), embedder });
|
|
451
|
+
* ```
|
|
452
|
+
*
|
|
453
|
+
* @example The Gemini API door, and a store that must never hold a clipped vector
|
|
454
|
+
* ```ts
|
|
455
|
+
* const embedder = geminiEmbedder({
|
|
456
|
+
* apiKey: process.env.GEMINI_API_KEY,
|
|
457
|
+
* onTruncation: 'refuse', // the default — stated here because it is the point
|
|
458
|
+
* });
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
export declare function geminiEmbedder(options?: GeminiEmbedderOptions): Embedder;
|
|
275
462
|
/**
|
|
276
463
|
* The slice of `@huggingface/transformers` {@link localEmbedder} uses.
|
|
277
464
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lazyRequire } from '../lib/lazyRequire.js';
|
|
2
|
+
import { resolveGoogleGenAIClient, } from '../adapters/llm/googleGenAI.js';
|
|
2
3
|
/**
|
|
3
4
|
* Native output size of every OpenAI embedding model, so `.dimensions` reports
|
|
4
5
|
* the truth instead of one hard-coded guess.
|
|
@@ -543,6 +544,236 @@ function describeShape(value) {
|
|
|
543
544
|
}
|
|
544
545
|
return typeof value;
|
|
545
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* The Gemini embedding models this library knows by name.
|
|
549
|
+
*
|
|
550
|
+
* **`gemini-embedding-001`** — text only, 3072 dimensions, a **2,048-token**
|
|
551
|
+
* input window, and the full `task_type` vocabulary. It is the default because
|
|
552
|
+
* this library's two call sites are a query and a passage, and the query /
|
|
553
|
+
* document task types are exactly that distinction.
|
|
554
|
+
*
|
|
555
|
+
* **`gemini-embedding-2`** — natively multimodal, 3072 dimensions, an
|
|
556
|
+
* **8,192-token** text window, and **no `task_type` parameter at all**: Google
|
|
557
|
+
* replaced it with task instructions written into the prompt. That difference
|
|
558
|
+
* is why the field is a per-model fact here rather than a constant — asked for
|
|
559
|
+
* on the model that does not take it, `task_type` is a request field the
|
|
560
|
+
* service will reject, and this adapter refuses first with the reason.
|
|
561
|
+
*
|
|
562
|
+
* A model outside this table has a length this library cannot know and must
|
|
563
|
+
* state its own — the same rule `openaiEmbedder` and `bedrockEmbedder` apply,
|
|
564
|
+
* for the same reason.
|
|
565
|
+
*/
|
|
566
|
+
const GEMINI_EMBEDDING_MODELS = {
|
|
567
|
+
'gemini-embedding-001': { dimensions: 3072, maxInputTokens: 2048, supportsTaskType: true },
|
|
568
|
+
'gemini-embedding-2': { dimensions: 3072, maxInputTokens: 8192, supportsTaskType: false },
|
|
569
|
+
};
|
|
570
|
+
/**
|
|
571
|
+
* How many texts Gemini embeds in ONE `embedContent` call: **one**.
|
|
572
|
+
*
|
|
573
|
+
* Not a conservative choice — the documented limit. `gemini-embedding-001`
|
|
574
|
+
* accepts exactly one input text per request, and libraries that assumed
|
|
575
|
+
* otherwise (batching 96 or 250 instances the way an OpenAI or Titan client
|
|
576
|
+
* would) send an oversized request that the service rejects on every batch
|
|
577
|
+
* bigger than a single text. `embedBatch` is therefore N sequential calls, and
|
|
578
|
+
* says so rather than looking like a batch that is secretly a loop.
|
|
579
|
+
*/
|
|
580
|
+
const GEMINI_MAX_TEXTS_PER_CALL = 1;
|
|
581
|
+
/**
|
|
582
|
+
* Google's hosted embeddings, through `models.embedContent` — on Vertex or on
|
|
583
|
+
* the Gemini API.
|
|
584
|
+
*
|
|
585
|
+
* The door is chosen exactly as `gemini()` chooses it: `{ project, location }`
|
|
586
|
+
* is Vertex with Application Default Credentials, `{ apiKey }` is the Gemini
|
|
587
|
+
* API, and neither is guessed. One `GoogleGenAI` client serves both this and
|
|
588
|
+
* the LLM provider, so an app that talks to both can build the client once and
|
|
589
|
+
* pass it as `_client`.
|
|
590
|
+
*
|
|
591
|
+
* ── Why the id carries the DIMENSION COUNT ───────────────────────────────
|
|
592
|
+
* `'gemini:<model>:<dims>'`, for the reason `bedrockEmbedder` gives at length:
|
|
593
|
+
* `MemoryEntry.embeddingModel` stores the id ALONE and it is the only thing
|
|
594
|
+
* `SearchOptions.embedderId` filters on. One model id at 768 and the same model
|
|
595
|
+
* id at 3072 are different embedding spaces, so an id without the size cannot
|
|
596
|
+
* tell them apart — and neither can the size alone, since both known models are
|
|
597
|
+
* 3072 natively.
|
|
598
|
+
*
|
|
599
|
+
* The `taskType` is deliberately NOT in the id, matching `bedrockEmbedder`'s
|
|
600
|
+
* treatment of Cohere's `input_type`: the default query/document pair is ONE
|
|
601
|
+
* space by construction (the two projections exist to be compared with each
|
|
602
|
+
* other), and a pinned value pins both call sites at once, so the store stays
|
|
603
|
+
* self-consistent either way.
|
|
604
|
+
*
|
|
605
|
+
* ── The input ceiling, and the service telling on itself (9.1.0) ─────────
|
|
606
|
+
* `.maxInputChars` is per MODEL, from its documented token window at the stated
|
|
607
|
+
* {@link CHARS_PER_TOKEN} assumption: **8,000** for `gemini-embedding-001`
|
|
608
|
+
* (2,048 tokens) and **32,000** for `gemini-embedding-2` (8,192). A quarter of
|
|
609
|
+
* the newer model's window is not a rounding difference — it is the whole
|
|
610
|
+
* reason the ceiling is per-model. Past the ceiling Gemini CLIPS rather than
|
|
611
|
+
* refuses, so `onTruncation` decides what happens when the response admits it.
|
|
612
|
+
*
|
|
613
|
+
* @throws if `model` is unknown and `dimensions` was not supplied; if
|
|
614
|
+
* `dimensions` exceeds what the model can produce; if `taskType` is set
|
|
615
|
+
* on a model that takes none; if neither a project nor an API key is
|
|
616
|
+
* resolvable; or if `@google/genai` is not installed and no `_client`
|
|
617
|
+
* was passed.
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* ```ts
|
|
621
|
+
* import { geminiEmbedder } from 'agentfootprint/providers';
|
|
622
|
+
* import { sqliteVectorStore } from 'agentfootprint/memory';
|
|
623
|
+
* import { indexFolder } from 'agentfootprint/rag';
|
|
624
|
+
*
|
|
625
|
+
* const embedder = geminiEmbedder({ project: 'my-project', dimensions: 768 });
|
|
626
|
+
* await indexFolder('./docs', { to: sqliteVectorStore({ file: './corpus.db' }), embedder });
|
|
627
|
+
* ```
|
|
628
|
+
*
|
|
629
|
+
* @example The Gemini API door, and a store that must never hold a clipped vector
|
|
630
|
+
* ```ts
|
|
631
|
+
* const embedder = geminiEmbedder({
|
|
632
|
+
* apiKey: process.env.GEMINI_API_KEY,
|
|
633
|
+
* onTruncation: 'refuse', // the default — stated here because it is the point
|
|
634
|
+
* });
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
export function geminiEmbedder(options = {}) {
|
|
638
|
+
const model = options.model ?? 'gemini-embedding-001';
|
|
639
|
+
const facts = GEMINI_EMBEDDING_MODELS[model];
|
|
640
|
+
const requested = options.dimensions;
|
|
641
|
+
const dimensions = requested ?? facts?.dimensions;
|
|
642
|
+
if (dimensions === undefined) {
|
|
643
|
+
throw new Error(`geminiEmbedder: unknown model '${model}' — its vector length is not something this ` +
|
|
644
|
+
`library can know, and reporting a wrong .dimensions silently corrupts a vector store. ` +
|
|
645
|
+
`Pass { dimensions } with the length that model returns.`);
|
|
646
|
+
}
|
|
647
|
+
if (requested !== undefined && (!Number.isInteger(requested) || requested < 1)) {
|
|
648
|
+
throw new Error(`geminiEmbedder: \`dimensions\` must be a positive whole number — received ` +
|
|
649
|
+
`${String(requested)}.`);
|
|
650
|
+
}
|
|
651
|
+
if (requested !== undefined && facts !== undefined && requested > facts.dimensions) {
|
|
652
|
+
throw new Error(`geminiEmbedder: '${model}' produces at most ${facts.dimensions} dimensions — received ` +
|
|
653
|
+
`${String(requested)}. Asking for more would be reported as .dimensions and never be ` +
|
|
654
|
+
`the length that comes back.`);
|
|
655
|
+
}
|
|
656
|
+
if (options.taskType !== undefined && facts !== undefined && !facts.supportsTaskType) {
|
|
657
|
+
throw new Error(`geminiEmbedder: '${model}' takes no \`task_type\` — Google replaced the parameter with ` +
|
|
658
|
+
`task instructions written into the text itself on this model. Drop \`taskType\` and ` +
|
|
659
|
+
`prefix your input (for example "task: search result | query: …"), or use ` +
|
|
660
|
+
`'gemini-embedding-001', which does take it.`);
|
|
661
|
+
}
|
|
662
|
+
const sendTaskType = facts?.supportsTaskType ?? true;
|
|
663
|
+
const maxInputChars = options.maxInputChars ?? charsFor(facts?.maxInputTokens);
|
|
664
|
+
const onTruncation = options.onTruncation ?? 'refuse';
|
|
665
|
+
let client;
|
|
666
|
+
const connect = () => {
|
|
667
|
+
client ??= resolveGoogleGenAIClient(options, 'geminiEmbedder', options._client);
|
|
668
|
+
return client;
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* ONE `embedContent` round-trip, carrying however many texts the model takes
|
|
672
|
+
* ({@link GEMINI_MAX_TEXTS_PER_CALL}) — one, today. Written as a slice rather
|
|
673
|
+
* than a single string so that the day Google raises the limit is a
|
|
674
|
+
* one-number change here and nowhere else.
|
|
675
|
+
*/
|
|
676
|
+
async function embedChunk(texts, taskType, signal) {
|
|
677
|
+
const config = {
|
|
678
|
+
...(sendTaskType && { taskType: options.taskType ?? taskType }),
|
|
679
|
+
...(requested !== undefined && { outputDimensionality: requested }),
|
|
680
|
+
...(signal && { abortSignal: signal }),
|
|
681
|
+
};
|
|
682
|
+
const response = await connect().models.embedContent({
|
|
683
|
+
model,
|
|
684
|
+
contents: [...texts],
|
|
685
|
+
...(Object.keys(config).length > 0 && { config }),
|
|
686
|
+
});
|
|
687
|
+
return readGeminiEmbeddings(response, model, dimensions, texts, onTruncation);
|
|
688
|
+
}
|
|
689
|
+
/** The calls one batch becomes, in order, one chunk at a time. */
|
|
690
|
+
async function embedAll(texts, taskType, signal) {
|
|
691
|
+
const out = [];
|
|
692
|
+
for (let i = 0; i < texts.length; i += GEMINI_MAX_TEXTS_PER_CALL) {
|
|
693
|
+
signal?.throwIfAborted();
|
|
694
|
+
out.push(...(await embedChunk(texts.slice(i, i + GEMINI_MAX_TEXTS_PER_CALL), taskType, signal)));
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
dimensions,
|
|
700
|
+
// The embedding SPACE, size included — see the note above.
|
|
701
|
+
id: `gemini:${model}:${dimensions}`,
|
|
702
|
+
// Spread, so a model this factory does not know declares NO ceiling rather
|
|
703
|
+
// than one it cannot stand behind.
|
|
704
|
+
...(maxInputChars !== undefined && { maxInputChars }),
|
|
705
|
+
async embed({ text, signal }) {
|
|
706
|
+
// ONE text is this library's query shape (`loadRelevant` embeds the
|
|
707
|
+
// question).
|
|
708
|
+
return (await embedChunk([text], 'RETRIEVAL_QUERY', signal))[0];
|
|
709
|
+
},
|
|
710
|
+
async embedBatch({ texts, signal }) {
|
|
711
|
+
// MANY texts is this library's document shape, and Gemini embeds a
|
|
712
|
+
// document into a different place from a query on purpose.
|
|
713
|
+
//
|
|
714
|
+
// Sequential rather than parallel: callers of this path are usually
|
|
715
|
+
// indexing a whole corpus, and `indexCorpus` already fans out over
|
|
716
|
+
// batches with its own bounded parallelism and retry. Racing N more
|
|
717
|
+
// requests inside one of those branches is how a corpus index meets a
|
|
718
|
+
// throttling error. The abort is checked between calls as well as passed
|
|
719
|
+
// into each one.
|
|
720
|
+
return embedAll(texts, 'RETRIEVAL_DOCUMENT', signal);
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Read the vectors out of an `embedContent` response — and refuse everything
|
|
726
|
+
* else by name.
|
|
727
|
+
*
|
|
728
|
+
* Four refusals, each for a failure that would otherwise be silent:
|
|
729
|
+
* • a count that disagrees with the texts sent — vectors are paired with
|
|
730
|
+
* texts by POSITION, so a short answer does not lose one passage, it
|
|
731
|
+
* re-labels every later one;
|
|
732
|
+
* • no readable `values` — an embedder that returns `[]` writes a row a store
|
|
733
|
+
* will never rank, which is indistinguishable from "the corpus does not
|
|
734
|
+
* mention that";
|
|
735
|
+
* • a length that disagrees with the declared `.dimensions` — the number a
|
|
736
|
+
* vector store fingerprints on;
|
|
737
|
+
* • `statistics.truncated` under `onTruncation: 'refuse'` — the service
|
|
738
|
+
* saying it indexed the opening of the passage and threw the rest away.
|
|
739
|
+
*/
|
|
740
|
+
function readGeminiEmbeddings(response, model, dimensions, texts, onTruncation) {
|
|
741
|
+
const rows = response.embeddings;
|
|
742
|
+
if (!Array.isArray(rows) || rows.length !== texts.length) {
|
|
743
|
+
throw new Error(`geminiEmbedder: '${model}' was sent ${texts.length} text(s) and answered with ` +
|
|
744
|
+
`${Array.isArray(rows) ? rows.length : describeShape(response)} — vectors are paired ` +
|
|
745
|
+
`with texts by POSITION, so a mismatched answer would attach every later vector to the ` +
|
|
746
|
+
`wrong passage, and the corpus would rank confidently and wrongly forever.`);
|
|
747
|
+
}
|
|
748
|
+
return rows.map((row, index) => {
|
|
749
|
+
const values = row?.values;
|
|
750
|
+
if (!Array.isArray(values) || !values.every((n) => typeof n === 'number')) {
|
|
751
|
+
throw new Error(`geminiEmbedder: '${model}' returned no \`embeddings[${index}].values\` array. The ` +
|
|
752
|
+
`service answered with ${describeShape(response)}, which this adapter cannot read — ` +
|
|
753
|
+
`check that the model id names an EMBEDDING model.`);
|
|
754
|
+
}
|
|
755
|
+
if (values.length !== dimensions) {
|
|
756
|
+
throw new Error(`geminiEmbedder: '${model}' answered with a ${values.length}-number vector while this ` +
|
|
757
|
+
`embedder reports .dimensions ${dimensions}. A store fingerprints on the reported ` +
|
|
758
|
+
`number, so writing this vector would make the store's own record of its shape a lie. ` +
|
|
759
|
+
`Pass { dimensions: ${values.length} } if that is the size you meant.`);
|
|
760
|
+
}
|
|
761
|
+
if (row?.statistics?.truncated === true && onTruncation === 'refuse') {
|
|
762
|
+
const chars = (texts[index] ?? '').length;
|
|
763
|
+
throw new Error(`geminiEmbedder: '${model}' CLIPPED this input — it reported ` +
|
|
764
|
+
`\`statistics.truncated\`, so the vector represents only the opening of the ` +
|
|
765
|
+
`${chars.toLocaleString('en-US')}-character text it was given. Storing it would ` +
|
|
766
|
+
`index a passage by a prefix of itself, and retrieval would then fail to find words ` +
|
|
767
|
+
`the passage visibly contains.\n` +
|
|
768
|
+
` Fix 1 — cut smaller: pass \`maxChunkChars\` to the indexer (it wins over this ` +
|
|
769
|
+
`embedder's declared .maxInputChars, which is a characters-per-token ASSUMPTION and ` +
|
|
770
|
+
`is optimistic for code, tables and CJK).\n` +
|
|
771
|
+
` Fix 2 — \`onTruncation: 'allow'\` if a prefix embedding is genuinely what you ` +
|
|
772
|
+
`want. The vector is returned unchanged; only this refusal goes away.`);
|
|
773
|
+
}
|
|
774
|
+
return [...values];
|
|
775
|
+
});
|
|
776
|
+
}
|
|
546
777
|
/**
|
|
547
778
|
* The MEASURED cliff of the default model, and the origin of the whole
|
|
548
779
|
* mechanism: `Xenova/all-MiniLM-L6-v2` silently truncates at 512 wordpiece
|