@tanstack/ai-bedrock 0.1.6 → 0.2.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.
@@ -1,6 +1,7 @@
1
1
  import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated.js';
2
2
  import { BedrockTextProviderOptions } from './text/text-provider-options.js';
3
3
  import { BedrockConverseProviderOptions } from './converse/provider-options.js';
4
+ import { BedrockCohereEmbeddingProviderOptions, BedrockEmbeddingProviderOptions, BedrockTitanImageEmbeddingProviderOptions, BedrockTitanTextEmbeddingProviderOptions } from './embedding/embedding-provider-options.js';
4
5
  type Entry = (typeof GENERATED_BEDROCK_MODELS)[number];
5
6
  /**
6
7
  * Type-level per-API filter over the generated catalog. Because the catalog is
@@ -35,4 +36,34 @@ export type BedrockChatModelToolCapabilitiesByName = {
35
36
  export type ResolveProviderOptions<TModel extends string> = TModel extends keyof BedrockChatModelProviderOptionsByName ? BedrockChatModelProviderOptionsByName[TModel] : BedrockTextProviderOptions;
36
37
  export type ResolveConverseProviderOptions<TModel extends string> = TModel extends keyof BedrockConverseModelProviderOptionsByName ? BedrockConverseModelProviderOptionsByName[TModel] : BedrockConverseProviderOptions;
37
38
  export type ResolveInputModalities<TModel extends string> = TModel extends keyof BedrockModelInputModalitiesByName ? BedrockModelInputModalitiesByName[TModel] : readonly ['text'];
39
+ /**
40
+ * Embedding models reachable through Bedrock's `InvokeModel` API. These are
41
+ * not part of the generated Converse catalog (embedding models have no
42
+ * conversational surface), so they're maintained by hand here.
43
+ */
44
+ export declare const BEDROCK_EMBEDDING_MODELS: readonly ["amazon.titan-embed-text-v2:0", "amazon.titan-embed-image-v1", "cohere.embed-english-v3", "cohere.embed-multilingual-v3"];
45
+ export type BedrockEmbeddingModel = (typeof BEDROCK_EMBEDDING_MODELS)[number];
46
+ /**
47
+ * Type-only map from embedding model name to its provider options type.
48
+ * The Cohere models make `modelOptions` REQUIRED at the `embed()` call site
49
+ * because `inputType` is a required field.
50
+ */
51
+ export type BedrockEmbeddingModelProviderOptionsByName = {
52
+ 'amazon.titan-embed-text-v2:0': BedrockTitanTextEmbeddingProviderOptions;
53
+ 'amazon.titan-embed-image-v1': BedrockTitanImageEmbeddingProviderOptions;
54
+ 'cohere.embed-english-v3': BedrockCohereEmbeddingProviderOptions;
55
+ 'cohere.embed-multilingual-v3': BedrockCohereEmbeddingProviderOptions;
56
+ };
57
+ /**
58
+ * Per-model input modalities for embedding models. Titan Multimodal accepts
59
+ * text and/or images (including fused text+image items embedded into one
60
+ * vector); the rest are text-only, so image inputs fail at compile time.
61
+ */
62
+ export type BedrockEmbeddingModelInputModalitiesByName = {
63
+ 'amazon.titan-embed-text-v2:0': readonly ['text'];
64
+ 'amazon.titan-embed-image-v1': readonly ['text', 'image'];
65
+ 'cohere.embed-english-v3': readonly ['text'];
66
+ 'cohere.embed-multilingual-v3': readonly ['text'];
67
+ };
68
+ export type ResolveEmbeddingProviderOptions<TModel extends string> = TModel extends keyof BedrockEmbeddingModelProviderOptionsByName ? BedrockEmbeddingModelProviderOptionsByName[TModel] : BedrockEmbeddingProviderOptions;
38
69
  export {};
@@ -4,7 +4,18 @@ import { GENERATED_BEDROCK_MODELS } from "./model-catalog.generated.js";
4
4
  var BEDROCK_CONVERSE_MODELS = GENERATED_BEDROCK_MODELS.map((m) => m.id);
5
5
  var BEDROCK_CHAT_MODELS = GENERATED_BEDROCK_MODELS.filter((m) => m.apis.chat).map((m) => m.id);
6
6
  var BEDROCK_RESPONSES_MODELS = GENERATED_BEDROCK_MODELS.filter((m) => m.apis.responses).map((m) => m.id);
7
+ /**
8
+ * Embedding models reachable through Bedrock's `InvokeModel` API. These are
9
+ * not part of the generated Converse catalog (embedding models have no
10
+ * conversational surface), so they're maintained by hand here.
11
+ */
12
+ var BEDROCK_EMBEDDING_MODELS = [
13
+ "amazon.titan-embed-text-v2:0",
14
+ "amazon.titan-embed-image-v1",
15
+ "cohere.embed-english-v3",
16
+ "cohere.embed-multilingual-v3"
17
+ ];
7
18
  //#endregion
8
- export { BEDROCK_CHAT_MODELS, BEDROCK_CONVERSE_MODELS, BEDROCK_RESPONSES_MODELS };
19
+ export { BEDROCK_CHAT_MODELS, BEDROCK_CONVERSE_MODELS, BEDROCK_EMBEDDING_MODELS, BEDROCK_RESPONSES_MODELS };
9
20
 
10
21
  //# sourceMappingURL=model-meta.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-meta.js","names":[],"sources":["../../src/model-meta.ts"],"sourcesContent":["import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated.js'\nimport type { BedrockTextProviderOptions } from './text/text-provider-options'\nimport type { BedrockConverseProviderOptions } from './converse/provider-options'\n\ntype Entry = (typeof GENERATED_BEDROCK_MODELS)[number]\n\n/**\n * Type-level per-API filter over the generated catalog. Because the catalog is\n * `as const`, `Extract` preserves literal `id` unions (no widening to `string`).\n */\ntype IdsWhere<TApi extends 'converse' | 'chat' | 'responses'> = Extract<\n Entry,\n { apis: Record<TApi, true> }\n>['id']\n\nexport type BedrockConverseModels = IdsWhere<'converse'>\nexport type BedrockChatModels = IdsWhere<'chat'>\nexport type BedrockResponsesModels = IdsWhere<'responses'>\n\n/** Runtime catalogs. Cast-free narrowing via a type predicate (the ai-bedrock pattern). */\n// Every catalog entry advertises `converse: true` (Converse is the universal\n// Bedrock surface), so the id list is the full catalog — no runtime filter needed.\nexport const BEDROCK_CONVERSE_MODELS: ReadonlyArray<BedrockConverseModels> =\n GENERATED_BEDROCK_MODELS.map((m) => m.id)\n\nexport const BEDROCK_CHAT_MODELS: ReadonlyArray<BedrockChatModels> =\n GENERATED_BEDROCK_MODELS.filter(\n (m): m is Extract<Entry, { apis: { chat: true } }> => m.apis.chat,\n ).map((m) => m.id)\n\nexport const BEDROCK_RESPONSES_MODELS: ReadonlyArray<BedrockResponsesModels> =\n GENERATED_BEDROCK_MODELS.filter(\n (m): m is Extract<Entry, { apis: { responses: true } }> => m.apis.responses,\n ).map((m) => m.id)\n\n/** Per-model input modalities (drives type-safe multimodal content). Covers ALL models. */\nexport type BedrockModelInputModalitiesByName = {\n [E in Entry as E['id']]: E['input']\n}\n\n/** Provider options per model. Same options for every model; keyed over the full catalog. */\nexport type BedrockChatModelProviderOptionsByName = {\n [E in Entry as E['id']]: BedrockTextProviderOptions\n}\n\n/** Converse provider options per model (narrower than the Chat Completions set). */\nexport type BedrockConverseModelProviderOptionsByName = {\n [E in Entry as E['id']]: BedrockConverseProviderOptions\n}\n\n/** No provider-specific tools — empty tuple makes cross-provider ProviderTool a compile error. */\nexport type BedrockChatModelToolCapabilitiesByName = {\n [E in Entry as E['id']]: readonly []\n}\n\nexport type ResolveProviderOptions<TModel extends string> =\n TModel extends keyof BedrockChatModelProviderOptionsByName\n ? BedrockChatModelProviderOptionsByName[TModel]\n : BedrockTextProviderOptions\n\nexport type ResolveConverseProviderOptions<TModel extends string> =\n TModel extends keyof BedrockConverseModelProviderOptionsByName\n ? BedrockConverseModelProviderOptionsByName[TModel]\n : BedrockConverseProviderOptions\n\nexport type ResolveInputModalities<TModel extends string> =\n TModel extends keyof BedrockModelInputModalitiesByName\n ? BedrockModelInputModalitiesByName[TModel]\n : readonly ['text']\n"],"mappings":";;;AAsBA,IAAa,0BACX,yBAAyB,KAAK,MAAM,EAAE,EAAE;AAE1C,IAAa,sBACX,yBAAyB,QACtB,MAAqD,EAAE,KAAK,IAC/D,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE;AAEnB,IAAa,2BACX,yBAAyB,QACtB,MAA0D,EAAE,KAAK,SACpE,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE"}
1
+ {"version":3,"file":"model-meta.js","names":[],"sources":["../../src/model-meta.ts"],"sourcesContent":["import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated.js'\nimport type { BedrockTextProviderOptions } from './text/text-provider-options'\nimport type { BedrockConverseProviderOptions } from './converse/provider-options'\nimport type {\n BedrockCohereEmbeddingProviderOptions,\n BedrockEmbeddingProviderOptions,\n BedrockTitanImageEmbeddingProviderOptions,\n BedrockTitanTextEmbeddingProviderOptions,\n} from './embedding/embedding-provider-options'\n\ntype Entry = (typeof GENERATED_BEDROCK_MODELS)[number]\n\n/**\n * Type-level per-API filter over the generated catalog. Because the catalog is\n * `as const`, `Extract` preserves literal `id` unions (no widening to `string`).\n */\ntype IdsWhere<TApi extends 'converse' | 'chat' | 'responses'> = Extract<\n Entry,\n { apis: Record<TApi, true> }\n>['id']\n\nexport type BedrockConverseModels = IdsWhere<'converse'>\nexport type BedrockChatModels = IdsWhere<'chat'>\nexport type BedrockResponsesModels = IdsWhere<'responses'>\n\n/** Runtime catalogs. Cast-free narrowing via a type predicate (the ai-bedrock pattern). */\n// Every catalog entry advertises `converse: true` (Converse is the universal\n// Bedrock surface), so the id list is the full catalog — no runtime filter needed.\nexport const BEDROCK_CONVERSE_MODELS: ReadonlyArray<BedrockConverseModels> =\n GENERATED_BEDROCK_MODELS.map((m) => m.id)\n\nexport const BEDROCK_CHAT_MODELS: ReadonlyArray<BedrockChatModels> =\n GENERATED_BEDROCK_MODELS.filter(\n (m): m is Extract<Entry, { apis: { chat: true } }> => m.apis.chat,\n ).map((m) => m.id)\n\nexport const BEDROCK_RESPONSES_MODELS: ReadonlyArray<BedrockResponsesModels> =\n GENERATED_BEDROCK_MODELS.filter(\n (m): m is Extract<Entry, { apis: { responses: true } }> => m.apis.responses,\n ).map((m) => m.id)\n\n/** Per-model input modalities (drives type-safe multimodal content). Covers ALL models. */\nexport type BedrockModelInputModalitiesByName = {\n [E in Entry as E['id']]: E['input']\n}\n\n/** Provider options per model. Same options for every model; keyed over the full catalog. */\nexport type BedrockChatModelProviderOptionsByName = {\n [E in Entry as E['id']]: BedrockTextProviderOptions\n}\n\n/** Converse provider options per model (narrower than the Chat Completions set). */\nexport type BedrockConverseModelProviderOptionsByName = {\n [E in Entry as E['id']]: BedrockConverseProviderOptions\n}\n\n/** No provider-specific tools — empty tuple makes cross-provider ProviderTool a compile error. */\nexport type BedrockChatModelToolCapabilitiesByName = {\n [E in Entry as E['id']]: readonly []\n}\n\nexport type ResolveProviderOptions<TModel extends string> =\n TModel extends keyof BedrockChatModelProviderOptionsByName\n ? BedrockChatModelProviderOptionsByName[TModel]\n : BedrockTextProviderOptions\n\nexport type ResolveConverseProviderOptions<TModel extends string> =\n TModel extends keyof BedrockConverseModelProviderOptionsByName\n ? BedrockConverseModelProviderOptionsByName[TModel]\n : BedrockConverseProviderOptions\n\nexport type ResolveInputModalities<TModel extends string> =\n TModel extends keyof BedrockModelInputModalitiesByName\n ? BedrockModelInputModalitiesByName[TModel]\n : readonly ['text']\n\n// ============================================================================\n// Embedding models\n// ============================================================================\n\n/**\n * Embedding models reachable through Bedrock's `InvokeModel` API. These are\n * not part of the generated Converse catalog (embedding models have no\n * conversational surface), so they're maintained by hand here.\n */\nexport const BEDROCK_EMBEDDING_MODELS = [\n 'amazon.titan-embed-text-v2:0',\n 'amazon.titan-embed-image-v1',\n 'cohere.embed-english-v3',\n 'cohere.embed-multilingual-v3',\n] as const\n\nexport type BedrockEmbeddingModel = (typeof BEDROCK_EMBEDDING_MODELS)[number]\n\n/**\n * Type-only map from embedding model name to its provider options type.\n * The Cohere models make `modelOptions` REQUIRED at the `embed()` call site\n * because `inputType` is a required field.\n */\nexport type BedrockEmbeddingModelProviderOptionsByName = {\n 'amazon.titan-embed-text-v2:0': BedrockTitanTextEmbeddingProviderOptions\n 'amazon.titan-embed-image-v1': BedrockTitanImageEmbeddingProviderOptions\n 'cohere.embed-english-v3': BedrockCohereEmbeddingProviderOptions\n 'cohere.embed-multilingual-v3': BedrockCohereEmbeddingProviderOptions\n}\n\n/**\n * Per-model input modalities for embedding models. Titan Multimodal accepts\n * text and/or images (including fused text+image items embedded into one\n * vector); the rest are text-only, so image inputs fail at compile time.\n */\nexport type BedrockEmbeddingModelInputModalitiesByName = {\n 'amazon.titan-embed-text-v2:0': readonly ['text']\n 'amazon.titan-embed-image-v1': readonly ['text', 'image']\n 'cohere.embed-english-v3': readonly ['text']\n 'cohere.embed-multilingual-v3': readonly ['text']\n}\n\nexport type ResolveEmbeddingProviderOptions<TModel extends string> =\n TModel extends keyof BedrockEmbeddingModelProviderOptionsByName\n ? BedrockEmbeddingModelProviderOptionsByName[TModel]\n : BedrockEmbeddingProviderOptions\n"],"mappings":";;;AA4BA,IAAa,0BACX,yBAAyB,KAAK,MAAM,EAAE,EAAE;AAE1C,IAAa,sBACX,yBAAyB,QACtB,MAAqD,EAAE,KAAK,IAC/D,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE;AAEnB,IAAa,2BACX,yBAAyB,QACtB,MAA0D,EAAE,KAAK,SACpE,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE;;;;;;AA8CnB,IAAa,2BAA2B;CACtC;CACA;CACA;CACA;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-bedrock",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Amazon Bedrock adapter for TanStack AI — OpenAI-compatible chat, responses, tools, and reasoning.",
6
6
  "author": "",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "zod": "^4.0.0",
43
- "@tanstack/ai": "0.43.1"
43
+ "@tanstack/ai": "^0.44.0"
44
44
  },
45
45
  "dependencies": {
46
46
  "@aws-crypto/sha256-js": "^5.2.0",
@@ -49,7 +49,7 @@
49
49
  "@smithy/signature-v4": "^5.4.5",
50
50
  "@smithy/types": "^4.14.2",
51
51
  "openai": "^6.41.0",
52
- "@tanstack/openai-base": "0.9.10"
52
+ "@tanstack/openai-base": "^0.9.11"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vite build",
@@ -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
+ }