@tanstack/ai-bedrock 0.1.6 → 0.2.1

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.
@@ -0,0 +1,547 @@
1
+ import { BaseEmbeddingAdapter } from '@tanstack/ai/adapters'
2
+ import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
3
+ import {
4
+ requireTextOnlyEmbeddingInput,
5
+ resolveEmbeddingInput,
6
+ } from '@tanstack/ai'
7
+ import { resolveBedrockAuth } from '../utils/auth'
8
+ import { BEDROCK_EMBEDDING_MODELS } from '../model-meta'
9
+ import type * as BedrockRuntime from '@aws-sdk/client-bedrock-runtime'
10
+ import type {
11
+ BedrockRuntimeClient,
12
+ BedrockRuntimeClientConfig,
13
+ } from '@aws-sdk/client-bedrock-runtime'
14
+ import type {
15
+ EmbeddingOptions,
16
+ EmbeddingResult,
17
+ ImagePart,
18
+ TokenUsage,
19
+ } from '@tanstack/ai'
20
+ import type { ResolvedBedrockAuth } from '../utils/auth'
21
+ import type { BedrockClientConfig } from '../utils/client'
22
+ import type {
23
+ BedrockEmbeddingModel,
24
+ BedrockEmbeddingModelInputModalitiesByName,
25
+ BedrockEmbeddingModelProviderOptionsByName,
26
+ ResolveEmbeddingProviderOptions,
27
+ } from '../model-meta'
28
+
29
+ /**
30
+ * Config for the Bedrock embedding adapter — the same auth surface as the
31
+ * other Bedrock adapters (apiKey → env → SigV4 via `resolveBedrockAuth`),
32
+ * minus the OpenAI-compat client options that don't apply to `InvokeModel`.
33
+ */
34
+ export interface BedrockEmbeddingConfig extends Pick<
35
+ BedrockClientConfig,
36
+ 'apiKey' | 'region' | 'auth' | 'baseURL'
37
+ > {}
38
+
39
+ /** InvokeModel calls issued concurrently during a per-item fan-out. */
40
+ const MAX_CONCURRENT_INVOCATIONS = 5
41
+
42
+ /** Valid `dimensions` for `amazon.titan-embed-text-v2:0`. */
43
+ const TITAN_TEXT_DIMENSIONS: ReadonlyArray<number> = [256, 512, 1024]
44
+
45
+ /** Valid `dimensions` (outputEmbeddingLength) for `amazon.titan-embed-image-v1`. */
46
+ const TITAN_IMAGE_DIMENSIONS: ReadonlyArray<number> = [256, 384, 1024]
47
+ const TITAN_IMAGE_DEFAULT_DIMENSIONS = 1024
48
+
49
+ /** Cohere embed accepts at most 96 texts per InvokeModel call. */
50
+ const COHERE_MAX_BATCH_SIZE = 96
51
+
52
+ /**
53
+ * Bedrock Embedding Adapter
54
+ *
55
+ * Tree-shakeable adapter for embeddings served through Bedrock's native
56
+ * `InvokeModel` API (embedding models have no Converse surface). Each model
57
+ * family has its own JSON body dialect:
58
+ *
59
+ * - `amazon.titan-embed-text-v2:0` — text-only, ONE text per call; the batch
60
+ * is fanned out with a small concurrency cap and per-call
61
+ * `inputTextTokenCount`s are summed into usage.
62
+ * - `amazon.titan-embed-image-v1` — MULTIMODAL: text, image, or a fused
63
+ * text+image item embedded into a single vector; one item per call.
64
+ * - `cohere.embed-english-v3` / `cohere.embed-multilingual-v3` — text-only,
65
+ * batched natively (chunked at 96 texts per call). `inputType` is required.
66
+ *
67
+ * The SDK call lives behind a protected `invokeModel` seam so tests can
68
+ * subclass and inject canned response bodies without a real AWS request, and
69
+ * the AWS SDK itself is imported lazily (it's Node/server-only).
70
+ */
71
+ export class BedrockEmbeddingAdapter<
72
+ TModel extends BedrockEmbeddingModel,
73
+ // Same rationale as the text adapters: the base parameterises
74
+ // `TProviderOptions extends object`, and the per-model options interfaces
75
+ // lack implicit index signatures — `Record<string, any>` (not `unknown`)
76
+ // accepts them. Confined to the generic constraint; no value cast.
77
+ TProviderOptions extends Record<string, any> =
78
+ ResolveEmbeddingProviderOptions<TModel>,
79
+ > extends BaseEmbeddingAdapter<
80
+ TModel,
81
+ TProviderOptions,
82
+ BedrockEmbeddingModelProviderOptionsByName,
83
+ BedrockEmbeddingModelInputModalitiesByName
84
+ > {
85
+ readonly name = 'bedrock' as const
86
+ private clientPromise?: Promise<BedrockRuntimeClient>
87
+ private readonly clientConfig: BedrockEmbeddingConfig
88
+
89
+ constructor(config: BedrockEmbeddingConfig, model: TModel) {
90
+ super(model, {})
91
+ // Defer client construction and auth resolution: the AWS SDK is Node/
92
+ // server-only, so we must not pull it into the static graph here. The
93
+ // client (and its dynamic import) is built lazily on first SDK call.
94
+ this.clientConfig = config
95
+ }
96
+
97
+ /**
98
+ * Dynamically import `@aws-sdk/client-bedrock-runtime`. The specifier is
99
+ * held in a variable (not a string literal) so bundler dep scanners cannot
100
+ * statically discover the AWS SDK and try to pre-bundle it for the browser.
101
+ * Same pattern as the Converse text adapter.
102
+ */
103
+ protected importBedrockRuntime(): Promise<typeof BedrockRuntime> {
104
+ const mod = '@aws-sdk/client-bedrock-runtime'
105
+ return import(/* @vite-ignore */ mod) as Promise<typeof BedrockRuntime>
106
+ }
107
+
108
+ /**
109
+ * Lazily construct the `BedrockRuntimeClient`, deferring
110
+ * `resolveBedrockAuth` until a real request is made.
111
+ */
112
+ protected async getClient(): Promise<BedrockRuntimeClient> {
113
+ if (!this.clientPromise) {
114
+ this.clientPromise = (async () => {
115
+ const { BedrockRuntimeClient } = await this.importBedrockRuntime()
116
+ const region = this.clientConfig.region ?? 'us-east-1'
117
+ const resolved = resolveBedrockAuth(
118
+ {
119
+ apiKey: this.clientConfig.apiKey,
120
+ region,
121
+ auth: this.clientConfig.auth,
122
+ },
123
+ 'runtime',
124
+ )
125
+ return new BedrockRuntimeClient(
126
+ this.buildClientConfig(resolved, region, this.clientConfig.baseURL),
127
+ )
128
+ })().catch((error: unknown) => {
129
+ // Don't cache a rejected promise — clear it so a later call can retry
130
+ // (e.g. after a transient import failure or fixed auth config).
131
+ this.clientPromise = undefined
132
+ throw error
133
+ })
134
+ }
135
+ return this.clientPromise
136
+ }
137
+
138
+ /**
139
+ * Map resolved auth + endpoint to a `BedrockRuntimeClientConfig`. Bearer
140
+ * auth needs `authSchemePreference` pinned or the SDK still tries SigV4
141
+ * first — same reasoning as the Converse text adapter.
142
+ */
143
+ protected buildClientConfig(
144
+ resolved: ResolvedBedrockAuth,
145
+ region: string,
146
+ endpoint: string | undefined,
147
+ ): BedrockRuntimeClientConfig {
148
+ if (resolved.kind === 'bearer') {
149
+ return {
150
+ region,
151
+ token: { token: resolved.token },
152
+ authSchemePreference: ['httpBearerAuth'],
153
+ ...(endpoint ? { endpoint } : {}),
154
+ }
155
+ }
156
+ return {
157
+ region: resolved.region,
158
+ credentials: resolved.credentials,
159
+ ...(endpoint ? { endpoint } : {}),
160
+ }
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // SDK seam (overridden in tests so no real AWS call happens)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /** Send one InvokeModel call and parse its JSON response body. */
168
+ protected async invokeModel(
169
+ modelId: string,
170
+ body: Record<string, unknown>,
171
+ ): Promise<unknown> {
172
+ const { InvokeModelCommand } = await this.importBedrockRuntime()
173
+ const client = await this.getClient()
174
+ const response = await client.send(
175
+ new InvokeModelCommand({
176
+ modelId,
177
+ contentType: 'application/json',
178
+ accept: 'application/json',
179
+ body: JSON.stringify(body),
180
+ }),
181
+ )
182
+ return JSON.parse(new TextDecoder().decode(response.body))
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Public adapter surface
187
+ // ---------------------------------------------------------------------------
188
+
189
+ async createEmbeddings(
190
+ options: EmbeddingOptions<TProviderOptions>,
191
+ ): Promise<EmbeddingResult> {
192
+ const { model, logger } = options
193
+ try {
194
+ logger.request(
195
+ `activity=embed provider=${this.name} model=${model} inputs=${options.input.length}`,
196
+ { provider: this.name, model },
197
+ )
198
+ switch (model) {
199
+ case 'amazon.titan-embed-text-v2:0':
200
+ return await this.embedTitanText(options)
201
+ case 'amazon.titan-embed-image-v1':
202
+ return await this.embedTitanImage(options)
203
+ case 'cohere.embed-english-v3':
204
+ case 'cohere.embed-multilingual-v3':
205
+ return await this.embedCohere(options)
206
+ default:
207
+ throw new Error(
208
+ `Unknown Bedrock embedding model "${model}". Supported models: ` +
209
+ `${BEDROCK_EMBEDDING_MODELS.join(', ')}.`,
210
+ )
211
+ }
212
+ } catch (error: unknown) {
213
+ logger.errors(`${this.name}.createEmbeddings fatal`, {
214
+ error: toRunErrorPayload(error, `${this.name}.createEmbeddings failed`),
215
+ source: `${this.name}.createEmbeddings`,
216
+ })
217
+ throw error
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Per-model request mapping
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * `amazon.titan-embed-text-v2:0` — one text per InvokeModel call, fanned
227
+ * out with a concurrency cap; result order matches input order and per-call
228
+ * `inputTextTokenCount`s are summed into usage.
229
+ */
230
+ private async embedTitanText(
231
+ options: EmbeddingOptions<TProviderOptions>,
232
+ ): Promise<EmbeddingResult> {
233
+ const { model, dimensions } = options
234
+ if (
235
+ dimensions !== undefined &&
236
+ !TITAN_TEXT_DIMENSIONS.includes(dimensions)
237
+ ) {
238
+ throw new Error(
239
+ `${model} supports dimensions 256, 512, or 1024; got ${dimensions}`,
240
+ )
241
+ }
242
+ const normalize: boolean | undefined = options.modelOptions?.normalize
243
+ const texts = requireTextOnlyEmbeddingInput(options.input, this.name, model)
244
+
245
+ const responses = await mapWithConcurrency(
246
+ texts,
247
+ MAX_CONCURRENT_INVOCATIONS,
248
+ async (text) => {
249
+ // Built incrementally: exactOptionalPropertyTypes is on, and Titan
250
+ // rejects explicit nulls/undefined for absent optional fields.
251
+ const body: Record<string, unknown> = { inputText: text }
252
+ if (dimensions !== undefined) body.dimensions = dimensions
253
+ if (normalize !== undefined) body.normalize = normalize
254
+ return readTitanEmbeddingBody(
255
+ await this.invokeModel(model, body),
256
+ `${this.name} ${model}`,
257
+ )
258
+ },
259
+ )
260
+
261
+ return this.toTitanResult(model, responses)
262
+ }
263
+
264
+ /**
265
+ * `amazon.titan-embed-image-v1` (Titan Multimodal) — one item per
266
+ * InvokeModel call. An item may carry text, an image, or both (a fused
267
+ * item embedded into a single vector). Titan accepts at most one image per
268
+ * request and never fetches remote URLs.
269
+ */
270
+ private async embedTitanImage(
271
+ options: EmbeddingOptions<TProviderOptions>,
272
+ ): Promise<EmbeddingResult> {
273
+ const { model, dimensions } = options
274
+ const outputEmbeddingLength = dimensions ?? TITAN_IMAGE_DEFAULT_DIMENSIONS
275
+ if (!TITAN_IMAGE_DIMENSIONS.includes(outputEmbeddingLength)) {
276
+ throw new Error(
277
+ `${model} supports dimensions 256, 384, or 1024; got ${outputEmbeddingLength}`,
278
+ )
279
+ }
280
+ const items = resolveEmbeddingInput(options.input)
281
+
282
+ const responses = await mapWithConcurrency(
283
+ items,
284
+ MAX_CONCURRENT_INVOCATIONS,
285
+ async (item, index) => {
286
+ if (item.images.length > 1) {
287
+ throw new Error(
288
+ `${model} accepts at most one image per input item; input item ` +
289
+ `at index ${index} contains ${item.images.length} images. ` +
290
+ `Pass them as separate input items (one vector each).`,
291
+ )
292
+ }
293
+ const body: Record<string, unknown> = {
294
+ embeddingConfig: { outputEmbeddingLength },
295
+ }
296
+ if (item.texts.length > 0) body.inputText = item.texts.join('\n')
297
+ const image = item.images[0]
298
+ if (image) body.inputImage = toTitanInputImage(image, model)
299
+ return readTitanEmbeddingBody(
300
+ await this.invokeModel(model, body),
301
+ `${this.name} ${model}`,
302
+ )
303
+ },
304
+ )
305
+
306
+ return this.toTitanResult(model, responses)
307
+ }
308
+
309
+ /**
310
+ * `cohere.embed-*-v3` — natively batched (chunked at 96 texts per call,
311
+ * order preserved across chunks). `inputType` is required by the Cohere
312
+ * API; output dimensionality is fixed, so `dimensions` is rejected.
313
+ */
314
+ private async embedCohere(
315
+ options: EmbeddingOptions<TProviderOptions>,
316
+ ): Promise<EmbeddingResult> {
317
+ const { model, dimensions } = options
318
+ if (dimensions !== undefined) {
319
+ throw new Error(
320
+ `${model} does not support the dimensions option; its output size is fixed`,
321
+ )
322
+ }
323
+ const inputType: string | undefined = options.modelOptions?.inputType
324
+ if (inputType === undefined) {
325
+ throw new Error(
326
+ `${model} requires modelOptions.inputType ('search_document' | ` +
327
+ `'search_query' | 'classification' | 'clustering')`,
328
+ )
329
+ }
330
+ const truncate: string | undefined = options.modelOptions?.truncate
331
+ const texts = requireTextOnlyEmbeddingInput(options.input, this.name, model)
332
+ const batches = chunk(texts, COHERE_MAX_BATCH_SIZE)
333
+
334
+ const responses = await mapWithConcurrency(
335
+ batches,
336
+ MAX_CONCURRENT_INVOCATIONS,
337
+ async (batch) => {
338
+ const body: Record<string, unknown> = {
339
+ texts: batch,
340
+ input_type: inputType,
341
+ }
342
+ if (truncate !== undefined) body.truncate = truncate
343
+ return readCohereEmbeddingBody(
344
+ await this.invokeModel(model, body),
345
+ `${this.name} ${model}`,
346
+ )
347
+ },
348
+ )
349
+
350
+ return {
351
+ id: this.generateId(),
352
+ model,
353
+ embeddings: responses.flat().map((vector, index) => ({ vector, index })),
354
+ }
355
+ }
356
+
357
+ /** Assemble an EmbeddingResult from per-item Titan responses. */
358
+ private toTitanResult(
359
+ model: string,
360
+ responses: Array<TitanEmbeddingBody>,
361
+ ): EmbeddingResult {
362
+ let promptTokens = 0
363
+ const embeddings = responses.map((response, index) => {
364
+ promptTokens += response.inputTextTokenCount
365
+ return { vector: response.embedding, index }
366
+ })
367
+ const usage: TokenUsage = {
368
+ promptTokens,
369
+ completionTokens: 0,
370
+ totalTokens: promptTokens,
371
+ }
372
+ return { id: this.generateId(), model, embeddings, usage }
373
+ }
374
+ }
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Response-body narrowing (SDK JSON boundary)
378
+ // ---------------------------------------------------------------------------
379
+
380
+ interface TitanEmbeddingBody {
381
+ embedding: Array<number>
382
+ /** 0 when the response omits it (e.g. image-only Titan Multimodal calls). */
383
+ inputTextTokenCount: number
384
+ }
385
+
386
+ function isRecord(value: unknown): value is Record<string, unknown> {
387
+ return typeof value === 'object' && value !== null
388
+ }
389
+
390
+ /** Narrow a Titan InvokeModel JSON body: `{ embedding, inputTextTokenCount? }`. */
391
+ function readTitanEmbeddingBody(
392
+ raw: unknown,
393
+ context: string,
394
+ ): TitanEmbeddingBody {
395
+ const embedding =
396
+ isRecord(raw) && Array.isArray(raw.embedding) ? raw.embedding : undefined
397
+ if (!embedding) {
398
+ throw new Error(
399
+ `${context}: response body is missing the "embedding" array`,
400
+ )
401
+ }
402
+ const inputTextTokenCount =
403
+ isRecord(raw) && typeof raw.inputTextTokenCount === 'number'
404
+ ? raw.inputTextTokenCount
405
+ : 0
406
+ return { embedding, inputTextTokenCount }
407
+ }
408
+
409
+ /** Narrow a Cohere InvokeModel JSON body: `{ embeddings: number[][] }` (float). */
410
+ function readCohereEmbeddingBody(
411
+ raw: unknown,
412
+ context: string,
413
+ ): Array<Array<number>> {
414
+ const embeddings =
415
+ isRecord(raw) && Array.isArray(raw.embeddings) ? raw.embeddings : undefined
416
+ if (!embeddings) {
417
+ throw new Error(
418
+ `${context}: response body is missing the "embeddings" array`,
419
+ )
420
+ }
421
+ return embeddings
422
+ }
423
+
424
+ // ---------------------------------------------------------------------------
425
+ // Input mapping helpers
426
+ // ---------------------------------------------------------------------------
427
+
428
+ /**
429
+ * Map an ImagePart to Titan's `inputImage` (RAW base64, no data: prefix).
430
+ * Accepts `data` sources as-is and `url` sources ONLY when the value is a
431
+ * `data:` URI; Titan cannot fetch remote http(s) URLs.
432
+ */
433
+ function toTitanInputImage(image: ImagePart, model: string): string {
434
+ const source = image.source
435
+ if (source.type === 'data') {
436
+ return source.value
437
+ }
438
+ if (source.value.startsWith('data:')) {
439
+ const comma = source.value.indexOf(',')
440
+ if (comma !== -1) {
441
+ return source.value.slice(comma + 1)
442
+ }
443
+ }
444
+ throw new Error(
445
+ `Bedrock Titan does not fetch remote image URLs; pass base64 data ` +
446
+ `(a { type: 'data' } source or a data: URI) for ${model}.`,
447
+ )
448
+ }
449
+
450
+ /** Split into runs of at most `size`, preserving order. */
451
+ function chunk<T>(items: Array<T>, size: number): Array<Array<T>> {
452
+ const chunks: Array<Array<T>> = []
453
+ for (let i = 0; i < items.length; i += size) {
454
+ chunks.push(items.slice(i, i + size))
455
+ }
456
+ return chunks
457
+ }
458
+
459
+ /**
460
+ * Map `fn` over `items` with at most `limit` calls in flight, returning
461
+ * results in input order (result[i] corresponds to items[i] regardless of
462
+ * completion order). Rejects with the first error.
463
+ */
464
+ async function mapWithConcurrency<T, TResult>(
465
+ items: ReadonlyArray<T>,
466
+ limit: number,
467
+ fn: (item: T, index: number) => Promise<TResult>,
468
+ ): Promise<Array<TResult>> {
469
+ const results = new Array<TResult>(items.length)
470
+ let next = 0
471
+ const worker = async (): Promise<void> => {
472
+ while (next < items.length) {
473
+ const index = next++
474
+ const item = items[index]
475
+ if (item === undefined) continue // unreachable: index < length
476
+ results[index] = await fn(item, index)
477
+ }
478
+ }
479
+ const workerCount = Math.max(1, Math.min(limit, items.length))
480
+ await Promise.all(Array.from({ length: workerCount }, () => worker()))
481
+ return results
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // Factories
486
+ // ---------------------------------------------------------------------------
487
+
488
+ /**
489
+ * Creates a Bedrock embedding adapter with an explicit API key (bearer).
490
+ * Type resolution happens here at the call site.
491
+ *
492
+ * @param model - The model name (e.g., 'amazon.titan-embed-text-v2:0')
493
+ * @param apiKey - Your Bedrock API key
494
+ * @param config - Optional additional configuration (region, baseURL, ...)
495
+ * @returns Configured Bedrock embedding adapter instance with resolved types
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * const adapter = createBedrockEmbedding(
500
+ * 'amazon.titan-embed-text-v2:0',
501
+ * 'bedrock-api-key',
502
+ * );
503
+ *
504
+ * const result = await embed({
505
+ * adapter,
506
+ * input: 'a red guitar',
507
+ * });
508
+ * ```
509
+ */
510
+ export function createBedrockEmbedding<TModel extends BedrockEmbeddingModel>(
511
+ model: TModel,
512
+ apiKey: string,
513
+ config?: Omit<BedrockEmbeddingConfig, 'apiKey'>,
514
+ ): BedrockEmbeddingAdapter<TModel> {
515
+ // Explicit apiKey is authoritative — spread config first so it can't override.
516
+ return new BedrockEmbeddingAdapter({ ...config, apiKey }, model)
517
+ }
518
+
519
+ /**
520
+ * Creates a Bedrock embedding adapter using the ambient auth cascade:
521
+ * `config.apiKey` → `BEDROCK_API_KEY` → `AWS_BEARER_TOKEN_BEDROCK` → SigV4
522
+ * (AWS credential provider chain). Auth resolves lazily on the first
523
+ * request, so `auth: 'sigv4'` never requires an API key.
524
+ *
525
+ * @param model - The model name (e.g., 'cohere.embed-english-v3')
526
+ * @param config - Optional configuration (region, auth, baseURL, ...)
527
+ * @returns Configured Bedrock embedding adapter instance with resolved types
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * const adapter = bedrockEmbedding('cohere.embed-english-v3');
532
+ *
533
+ * const result = await embed({
534
+ * adapter,
535
+ * input: ['a red guitar', 'a blue drum kit'],
536
+ * modelOptions: { inputType: 'search_document' },
537
+ * });
538
+ *
539
+ * console.log(result.embeddings[0].vector)
540
+ * ```
541
+ */
542
+ export function bedrockEmbedding<TModel extends BedrockEmbeddingModel>(
543
+ model: TModel,
544
+ config?: BedrockEmbeddingConfig,
545
+ ): BedrockEmbeddingAdapter<TModel> {
546
+ return new BedrockEmbeddingAdapter(config ?? {}, model)
547
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Provider options for the Bedrock embedding models.
3
+ *
4
+ * `dimensions` is deliberately absent from every options shape: it's a
5
+ * first-class top-level option on `embed()`. The adapter maps it onto each
6
+ * model's native field (Titan Text `dimensions`, Titan Multimodal
7
+ * `embeddingConfig.outputEmbeddingLength`) and rejects it for the Cohere
8
+ * models, whose output size is fixed.
9
+ */
10
+
11
+ /** Options for `amazon.titan-embed-text-v2:0`. */
12
+ export interface BedrockTitanTextEmbeddingProviderOptions {
13
+ /**
14
+ * Normalize the output embedding to unit length. Titan's service-side
15
+ * default is `true`; leave unset to use it.
16
+ */
17
+ normalize?: boolean
18
+ }
19
+
20
+ /**
21
+ * Options for `amazon.titan-embed-image-v1` (Titan Multimodal Embeddings).
22
+ * The model has no per-request options beyond the top-level `dimensions`
23
+ * (mapped onto `embeddingConfig.outputEmbeddingLength`).
24
+ */
25
+ export type BedrockTitanImageEmbeddingProviderOptions = Record<string, never>
26
+
27
+ /**
28
+ * Cohere embed input types. Cohere REQUIRES the caller to say what the
29
+ * embeddings are for; there is no service-side default.
30
+ */
31
+ export type BedrockCohereEmbeddingInputType =
32
+ | 'search_document'
33
+ | 'search_query'
34
+ | 'classification'
35
+ | 'clustering'
36
+
37
+ /** Options for `cohere.embed-english-v3` / `cohere.embed-multilingual-v3`. */
38
+ export interface BedrockCohereEmbeddingProviderOptions {
39
+ /**
40
+ * What the embeddings will be used for (required by the Cohere API; sent
41
+ * as `input_type`). Use `search_document` when indexing, `search_query`
42
+ * when querying, `classification` / `clustering` for those downstream
43
+ * tasks. Because this field is required, `modelOptions` is required at the
44
+ * `embed()` call site for the Cohere models.
45
+ */
46
+ inputType: BedrockCohereEmbeddingInputType
47
+ /**
48
+ * How to handle inputs longer than the model's maximum token length.
49
+ * `NONE` (the API default) returns an error, `START` / `END` truncate
50
+ * from that side.
51
+ */
52
+ truncate?: 'NONE' | 'START' | 'END'
53
+ }
54
+
55
+ /**
56
+ * Broad union used as the adapter's base `TProviderOptions` fallback when
57
+ * the model isn't statically known. Per-model narrowing happens via
58
+ * `BedrockEmbeddingModelProviderOptionsByName`.
59
+ */
60
+ export type BedrockEmbeddingProviderOptions =
61
+ | BedrockTitanTextEmbeddingProviderOptions
62
+ | BedrockTitanImageEmbeddingProviderOptions
63
+ | BedrockCohereEmbeddingProviderOptions
package/src/index.ts CHANGED
@@ -161,6 +161,19 @@ export {
161
161
  createBedrockConverse,
162
162
  type BedrockConverseConfig,
163
163
  } from './adapters/converse-text'
164
+ export {
165
+ BedrockEmbeddingAdapter,
166
+ bedrockEmbedding,
167
+ createBedrockEmbedding,
168
+ type BedrockEmbeddingConfig,
169
+ } from './adapters/embedding'
170
+ export type {
171
+ BedrockCohereEmbeddingInputType,
172
+ BedrockCohereEmbeddingProviderOptions,
173
+ BedrockEmbeddingProviderOptions,
174
+ BedrockTitanImageEmbeddingProviderOptions,
175
+ BedrockTitanTextEmbeddingProviderOptions,
176
+ } from './embedding/embedding-provider-options'
164
177
  export type { BedrockConverseProviderOptions } from './converse/provider-options'
165
178
  export {
166
179
  resolveBedrockAuth,
@@ -173,6 +186,10 @@ export {
173
186
  BEDROCK_CHAT_MODELS,
174
187
  BEDROCK_RESPONSES_MODELS,
175
188
  BEDROCK_CONVERSE_MODELS,
189
+ BEDROCK_EMBEDDING_MODELS,
190
+ type BedrockEmbeddingModel,
191
+ type BedrockEmbeddingModelProviderOptionsByName,
192
+ type BedrockEmbeddingModelInputModalitiesByName,
176
193
  type BedrockChatModels,
177
194
  type BedrockResponsesModels,
178
195
  type BedrockConverseModels,
package/src/model-meta.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated.js'
2
2
  import type { BedrockTextProviderOptions } from './text/text-provider-options'
3
3
  import type { BedrockConverseProviderOptions } from './converse/provider-options'
4
+ import type {
5
+ BedrockCohereEmbeddingProviderOptions,
6
+ BedrockEmbeddingProviderOptions,
7
+ BedrockTitanImageEmbeddingProviderOptions,
8
+ BedrockTitanTextEmbeddingProviderOptions,
9
+ } from './embedding/embedding-provider-options'
4
10
 
5
11
  type Entry = (typeof GENERATED_BEDROCK_MODELS)[number]
6
12
 
@@ -67,3 +73,50 @@ export type ResolveInputModalities<TModel extends string> =
67
73
  TModel extends keyof BedrockModelInputModalitiesByName
68
74
  ? BedrockModelInputModalitiesByName[TModel]
69
75
  : readonly ['text']
76
+
77
+ // ============================================================================
78
+ // Embedding models
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Embedding models reachable through Bedrock's `InvokeModel` API. These are
83
+ * not part of the generated Converse catalog (embedding models have no
84
+ * conversational surface), so they're maintained by hand here.
85
+ */
86
+ export const BEDROCK_EMBEDDING_MODELS = [
87
+ 'amazon.titan-embed-text-v2:0',
88
+ 'amazon.titan-embed-image-v1',
89
+ 'cohere.embed-english-v3',
90
+ 'cohere.embed-multilingual-v3',
91
+ ] as const
92
+
93
+ export type BedrockEmbeddingModel = (typeof BEDROCK_EMBEDDING_MODELS)[number]
94
+
95
+ /**
96
+ * Type-only map from embedding model name to its provider options type.
97
+ * The Cohere models make `modelOptions` REQUIRED at the `embed()` call site
98
+ * because `inputType` is a required field.
99
+ */
100
+ export type BedrockEmbeddingModelProviderOptionsByName = {
101
+ 'amazon.titan-embed-text-v2:0': BedrockTitanTextEmbeddingProviderOptions
102
+ 'amazon.titan-embed-image-v1': BedrockTitanImageEmbeddingProviderOptions
103
+ 'cohere.embed-english-v3': BedrockCohereEmbeddingProviderOptions
104
+ 'cohere.embed-multilingual-v3': BedrockCohereEmbeddingProviderOptions
105
+ }
106
+
107
+ /**
108
+ * Per-model input modalities for embedding models. Titan Multimodal accepts
109
+ * text and/or images (including fused text+image items embedded into one
110
+ * vector); the rest are text-only, so image inputs fail at compile time.
111
+ */
112
+ export type BedrockEmbeddingModelInputModalitiesByName = {
113
+ 'amazon.titan-embed-text-v2:0': readonly ['text']
114
+ 'amazon.titan-embed-image-v1': readonly ['text', 'image']
115
+ 'cohere.embed-english-v3': readonly ['text']
116
+ 'cohere.embed-multilingual-v3': readonly ['text']
117
+ }
118
+
119
+ export type ResolveEmbeddingProviderOptions<TModel extends string> =
120
+ TModel extends keyof BedrockEmbeddingModelProviderOptionsByName
121
+ ? BedrockEmbeddingModelProviderOptionsByName[TModel]
122
+ : BedrockEmbeddingProviderOptions