unrag 0.2.5 → 0.2.7

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.
Files changed (42) hide show
  1. package/dist/cli/index.js +611 -174
  2. package/package.json +12 -6
  3. package/registry/config/unrag.config.ts +9 -8
  4. package/registry/connectors/google-drive/_api-types.ts +60 -0
  5. package/registry/connectors/google-drive/client.ts +99 -38
  6. package/registry/connectors/google-drive/sync.ts +97 -69
  7. package/registry/connectors/google-drive/types.ts +76 -37
  8. package/registry/connectors/notion/client.ts +12 -3
  9. package/registry/connectors/notion/render.ts +62 -23
  10. package/registry/connectors/notion/sync.ts +30 -23
  11. package/registry/core/assets.ts +11 -10
  12. package/registry/core/config.ts +10 -25
  13. package/registry/core/context-engine.ts +71 -2
  14. package/registry/core/deep-merge.ts +45 -0
  15. package/registry/core/ingest.ts +117 -44
  16. package/registry/core/types.ts +96 -2
  17. package/registry/docs/unrag.md +6 -1
  18. package/registry/embedding/_shared.ts +25 -0
  19. package/registry/embedding/ai.ts +8 -68
  20. package/registry/embedding/azure.ts +88 -0
  21. package/registry/embedding/bedrock.ts +88 -0
  22. package/registry/embedding/cohere.ts +88 -0
  23. package/registry/embedding/google.ts +102 -0
  24. package/registry/embedding/mistral.ts +71 -0
  25. package/registry/embedding/ollama.ts +90 -0
  26. package/registry/embedding/openai.ts +88 -0
  27. package/registry/embedding/openrouter.ts +127 -0
  28. package/registry/embedding/together.ts +77 -0
  29. package/registry/embedding/vertex.ts +111 -0
  30. package/registry/embedding/voyage.ts +169 -0
  31. package/registry/extractors/audio-transcribe/index.ts +39 -23
  32. package/registry/extractors/file-docx/index.ts +8 -1
  33. package/registry/extractors/file-pptx/index.ts +22 -1
  34. package/registry/extractors/file-xlsx/index.ts +24 -1
  35. package/registry/extractors/image-caption-llm/index.ts +8 -3
  36. package/registry/extractors/image-ocr/index.ts +9 -4
  37. package/registry/extractors/pdf-llm/index.ts +9 -4
  38. package/registry/extractors/pdf-text-layer/index.ts +23 -2
  39. package/registry/extractors/video-frames/index.ts +8 -3
  40. package/registry/extractors/video-transcribe/index.ts +40 -24
  41. package/registry/manifest.json +346 -0
  42. package/registry/store/drizzle-postgres-pgvector/store.ts +26 -6
@@ -9,37 +9,13 @@ import type {
9
9
  IngestInput,
10
10
  IngestResult,
11
11
  IngestWarning,
12
+ Metadata,
12
13
  ResolvedContextEngineConfig,
13
14
  } from "./types";
15
+ import { mergeDeep } from "./deep-merge";
14
16
 
15
17
  const now = () => performance.now();
16
18
 
17
- const mergeDeep = <T extends Record<string, any>>(
18
- base: T,
19
- overrides: any | undefined
20
- ): T => {
21
- if (!overrides) return base;
22
- const out: any = Array.isArray(base) ? [...base] : { ...base };
23
- for (const key of Object.keys(overrides)) {
24
- const nextVal = overrides[key];
25
- if (nextVal === undefined) continue;
26
- const baseVal = (base as any)[key];
27
- if (
28
- baseVal &&
29
- typeof baseVal === "object" &&
30
- !Array.isArray(baseVal) &&
31
- nextVal &&
32
- typeof nextVal === "object" &&
33
- !Array.isArray(nextVal)
34
- ) {
35
- out[key] = mergeDeep(baseVal, nextVal);
36
- } else {
37
- out[key] = nextVal;
38
- }
39
- }
40
- return out as T;
41
- };
42
-
43
19
  const asMessage = (err: unknown) => {
44
20
  if (err instanceof Error) return err.message;
45
21
  try {
@@ -123,7 +99,7 @@ export const ingest = async (
123
99
 
124
100
  const assets: AssetInput[] = Array.isArray(input.assets) ? input.assets : [];
125
101
  type PreparedChunkSpec = Omit<Chunk, "id" | "index"> & {
126
- metadata: Record<string, any>;
102
+ metadata: Metadata;
127
103
  embed:
128
104
  | { kind: "text"; text: string }
129
105
  | { kind: "image"; data: Uint8Array | string; mediaType?: string; assetId?: string };
@@ -140,7 +116,7 @@ export const ingest = async (
140
116
 
141
117
  const runExtractors = async (args: {
142
118
  asset: AssetInput;
143
- assetMeta: Record<string, any>;
119
+ assetMeta: Metadata;
144
120
  assetUri?: string;
145
121
  assetMediaType?: string;
146
122
  extractors: AssetExtractor[];
@@ -528,14 +504,41 @@ export const ingest = async (
528
504
  const chunkingMs = now() - chunkingStart;
529
505
  const embeddingStart = now();
530
506
 
531
- const embeddedChunks = await Promise.all(
532
- prepared.map(async ({ chunk, embed }) => {
533
- if (embed.kind === "image") {
534
- const embedImage = config.embedding.embedImage;
535
- if (!embedImage) {
536
- throw new Error("Image embedding requested but provider does not support embedImage()");
537
- }
538
- const embedding = await embedImage({
507
+ const embeddedChunks: Chunk[] = new Array(prepared.length);
508
+
509
+ const textSpecs: Array<{
510
+ idx: number;
511
+ chunk: Chunk;
512
+ input: {
513
+ text: string;
514
+ metadata: Metadata;
515
+ position: number;
516
+ sourceId: string;
517
+ documentId: string;
518
+ };
519
+ }> = [];
520
+
521
+ const imageSpecs: Array<{
522
+ idx: number;
523
+ chunk: Chunk;
524
+ input: {
525
+ data: Uint8Array | string;
526
+ mediaType?: string;
527
+ metadata: Metadata;
528
+ position: number;
529
+ sourceId: string;
530
+ documentId: string;
531
+ assetId?: string;
532
+ };
533
+ }> = [];
534
+
535
+ for (let i = 0; i < prepared.length; i++) {
536
+ const { chunk, embed } = prepared[i]!;
537
+ if (embed.kind === "image") {
538
+ imageSpecs.push({
539
+ idx: i,
540
+ chunk,
541
+ input: {
539
542
  data: embed.data,
540
543
  mediaType: embed.mediaType,
541
544
  metadata: chunk.metadata,
@@ -543,21 +546,91 @@ export const ingest = async (
543
546
  sourceId: chunk.sourceId,
544
547
  documentId: chunk.documentId,
545
548
  assetId: embed.assetId,
546
- });
547
- return { ...chunk, embedding };
548
- }
549
+ },
550
+ });
551
+ continue;
552
+ }
549
553
 
550
- const embedding = await config.embedding.embed({
554
+ textSpecs.push({
555
+ idx: i,
556
+ chunk,
557
+ input: {
551
558
  text: embed.text,
552
559
  metadata: chunk.metadata,
553
560
  position: chunk.index,
554
561
  sourceId: chunk.sourceId,
555
562
  documentId: chunk.documentId,
556
- });
563
+ },
564
+ });
565
+ }
557
566
 
558
- return { ...chunk, embedding };
559
- })
560
- );
567
+ const concurrency = config.embeddingProcessing.concurrency;
568
+
569
+ // Text embeddings (prefer batch when supported).
570
+ if (textSpecs.length > 0) {
571
+ const embedMany = config.embedding.embedMany;
572
+ if (embedMany) {
573
+ const batchSize = Math.max(1, Math.floor(config.embeddingProcessing.batchSize || 1));
574
+ const batches: Array<typeof textSpecs> = [];
575
+ for (let i = 0; i < textSpecs.length; i += batchSize) {
576
+ batches.push(textSpecs.slice(i, i + batchSize));
577
+ }
578
+
579
+ const batchEmbeddings = await mapWithConcurrency(
580
+ batches,
581
+ concurrency,
582
+ async (batch) => {
583
+ const embeddings = await embedMany(batch.map((b) => b.input));
584
+ if (!Array.isArray(embeddings) || embeddings.length !== batch.length) {
585
+ throw new Error(
586
+ `embedMany() returned ${Array.isArray(embeddings) ? embeddings.length : "non-array"} embeddings for a batch of ${batch.length}`
587
+ );
588
+ }
589
+ return embeddings;
590
+ }
591
+ );
592
+
593
+ let batchIdx = 0;
594
+ for (const batch of batches) {
595
+ const embeddings = batchEmbeddings[batchIdx++]!;
596
+ for (let i = 0; i < batch.length; i++) {
597
+ const spec = batch[i]!;
598
+ embeddedChunks[spec.idx] = { ...spec.chunk, embedding: embeddings[i]! };
599
+ }
600
+ }
601
+ } else {
602
+ const embeddings = await mapWithConcurrency(textSpecs, concurrency, async (spec) =>
603
+ config.embedding.embed(spec.input)
604
+ );
605
+ for (let i = 0; i < textSpecs.length; i++) {
606
+ const spec = textSpecs[i]!;
607
+ embeddedChunks[spec.idx] = { ...spec.chunk, embedding: embeddings[i]! };
608
+ }
609
+ }
610
+ }
611
+
612
+ // Image embeddings (bounded concurrency).
613
+ if (imageSpecs.length > 0) {
614
+ const embedImage = config.embedding.embedImage;
615
+ if (!embedImage) {
616
+ throw new Error("Image embedding requested but provider does not support embedImage()");
617
+ }
618
+
619
+ const embeddings = await mapWithConcurrency(imageSpecs, concurrency, async (spec) =>
620
+ embedImage(spec.input)
621
+ );
622
+ for (let i = 0; i < imageSpecs.length; i++) {
623
+ const spec = imageSpecs[i]!;
624
+ embeddedChunks[spec.idx] = { ...spec.chunk, embedding: embeddings[i]! };
625
+ }
626
+ }
627
+
628
+ // Safety check: ensure all chunks got an embedding.
629
+ for (let i = 0; i < embeddedChunks.length; i++) {
630
+ if (!embeddedChunks[i]) {
631
+ throw new Error("Internal error: missing embedding for one or more chunks");
632
+ }
633
+ }
561
634
 
562
635
  const embeddingMs = now() - embeddingStart;
563
636
  const storageStart = now();
@@ -5,6 +5,30 @@ export type Metadata = Record<
5
5
  MetadataValue | MetadataValue[] | undefined
6
6
  >;
7
7
 
8
+ /**
9
+ * Standard fields for asset-related metadata.
10
+ * These are added to chunk metadata when chunks are derived from assets.
11
+ */
12
+ export interface AssetMetadataFields {
13
+ assetKind?: "image" | "pdf" | "audio" | "video" | "file";
14
+ assetId?: string;
15
+ assetUri?: string;
16
+ assetMediaType?: string;
17
+ extractor?: string;
18
+ }
19
+
20
+ /**
21
+ * Type guard for checking if metadata contains required asset fields.
22
+ */
23
+ export function hasAssetMetadata(
24
+ metadata: Metadata
25
+ ): metadata is Metadata & Required<Pick<AssetMetadataFields, "assetKind" | "assetId">> {
26
+ return (
27
+ typeof metadata.assetKind === "string" &&
28
+ typeof metadata.assetId === "string"
29
+ );
30
+ }
31
+
8
32
  export type Chunk = {
9
33
  id: string;
10
34
  documentId: string;
@@ -31,6 +55,24 @@ export type ContentStorageConfig = {
31
55
  storeDocumentContent: boolean;
32
56
  };
33
57
 
58
+ /**
59
+ * Controls performance characteristics of embedding during ingest.
60
+ *
61
+ * These defaults are intentionally conservative to reduce rate-limit risk.
62
+ */
63
+ export type EmbeddingProcessingConfig = {
64
+ /**
65
+ * Maximum number of concurrent embedding requests.
66
+ * This applies to both text embedding (embed/embedMany) and image embedding (embedImage).
67
+ */
68
+ concurrency: number;
69
+ /**
70
+ * Max number of text chunks per embedMany batch (when embedMany is supported).
71
+ * Ignored when the provider does not implement embedMany().
72
+ */
73
+ batchSize: number;
74
+ };
75
+
34
76
  export type ChunkText = {
35
77
  index: number;
36
78
  content: string;
@@ -655,6 +697,11 @@ export type RetrieveResult = {
655
697
  */
656
698
  export type UnragDefaultsConfig = {
657
699
  chunking?: Partial<ChunkingOptions>;
700
+ /**
701
+ * Embedding performance defaults (batching + concurrency).
702
+ * These map to the engine's `embeddingProcessing` config.
703
+ */
704
+ embedding?: Partial<EmbeddingProcessingConfig>;
658
705
  retrieval?: {
659
706
  topK?: number;
660
707
  };
@@ -670,6 +717,50 @@ export type UnragEmbeddingConfig =
670
717
  provider: "ai";
671
718
  config?: import("../embedding/ai").AiEmbeddingConfig;
672
719
  }
720
+ | {
721
+ provider: "openai";
722
+ config?: import("../embedding/openai").OpenAiEmbeddingConfig;
723
+ }
724
+ | {
725
+ provider: "google";
726
+ config?: import("../embedding/google").GoogleEmbeddingConfig;
727
+ }
728
+ | {
729
+ provider: "openrouter";
730
+ config?: import("../embedding/openrouter").OpenRouterEmbeddingConfig;
731
+ }
732
+ | {
733
+ provider: "azure";
734
+ config?: import("../embedding/azure").AzureEmbeddingConfig;
735
+ }
736
+ | {
737
+ provider: "vertex";
738
+ config?: import("../embedding/vertex").VertexEmbeddingConfig;
739
+ }
740
+ | {
741
+ provider: "bedrock";
742
+ config?: import("../embedding/bedrock").BedrockEmbeddingConfig;
743
+ }
744
+ | {
745
+ provider: "cohere";
746
+ config?: import("../embedding/cohere").CohereEmbeddingConfig;
747
+ }
748
+ | {
749
+ provider: "mistral";
750
+ config?: import("../embedding/mistral").MistralEmbeddingConfig;
751
+ }
752
+ | {
753
+ provider: "together";
754
+ config?: import("../embedding/together").TogetherEmbeddingConfig;
755
+ }
756
+ | {
757
+ provider: "ollama";
758
+ config?: import("../embedding/ollama").OllamaEmbeddingConfig;
759
+ }
760
+ | {
761
+ provider: "voyage";
762
+ config?: import("../embedding/voyage").VoyageEmbeddingConfig;
763
+ }
673
764
  | {
674
765
  provider: "custom";
675
766
  /**
@@ -724,6 +815,10 @@ export type ContextEngineConfig = {
724
815
  * captions, which can still be ingested via `assets[].text` if you choose).
725
816
  */
726
817
  assetProcessing?: DeepPartial<AssetProcessingConfig>;
818
+ /**
819
+ * Embedding performance defaults for ingest (batching + concurrency).
820
+ */
821
+ embeddingProcessing?: DeepPartial<EmbeddingProcessingConfig>;
727
822
  };
728
823
 
729
824
  export type ResolvedContextEngineConfig = {
@@ -735,6 +830,5 @@ export type ResolvedContextEngineConfig = {
735
830
  extractors: AssetExtractor[];
736
831
  storage: ContentStorageConfig;
737
832
  assetProcessing: AssetProcessingConfig;
833
+ embeddingProcessing: EmbeddingProcessingConfig;
738
834
  };
739
-
740
-
@@ -8,9 +8,14 @@ Unrag installs a small RAG module into your codebase with:
8
8
 
9
9
  Add these to your environment:
10
10
  - `DATABASE_URL` (Postgres connection string)
11
- - `AI_GATEWAY_API_KEY` (required by the `ai` SDK when using Vercel AI Gateway)
11
+ - (Embedding) set the environment variables required by your selected provider.
12
+
13
+ If you used the default provider (Vercel AI Gateway):
14
+ - `AI_GATEWAY_API_KEY`
12
15
  - Optional: `AI_GATEWAY_MODEL` (defaults to `openai/text-embedding-3-small`)
13
16
 
17
+ If you picked a different provider (OpenAI / Google / Voyage / etc.), see the installed provider docs under your Unrag docs site (`/docs/providers/*`).
18
+
14
19
  ## Database requirements
15
20
 
16
21
  Enable pgvector:
@@ -0,0 +1,25 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+
5
+ /**
6
+ * Dynamically require an optional dependency with type-safe return.
7
+ *
8
+ * @template T - The expected module type (callers must define this)
9
+ */
10
+ export function requireOptional<T>(args: {
11
+ id: string;
12
+ installHint: string;
13
+ providerName: string;
14
+ }): T {
15
+ try {
16
+ return require(args.id) as T;
17
+ } catch {
18
+ throw new Error(
19
+ `Unrag embedding provider "${args.providerName}" requires "${args.id}" to be installed.\n` +
20
+ `Install it with: ${args.installHint}`
21
+ );
22
+ }
23
+ }
24
+
25
+
@@ -1,5 +1,5 @@
1
1
  import { embed, embedMany } from "ai";
2
- import type { EmbeddingProvider, ImageEmbeddingInput } from "../core/types";
2
+ import type { EmbeddingProvider } from "../core/types";
3
3
 
4
4
  type BaseConfig = {
5
5
  /**
@@ -9,52 +9,18 @@ type BaseConfig = {
9
9
  timeoutMs?: number;
10
10
  };
11
11
 
12
- export type AiEmbeddingConfig =
13
- | (BaseConfig & {
14
- /**
15
- * Defaults to "text" for backwards compatibility.
16
- * - "text": only supports embedding strings
17
- * - "multimodal": additionally enables `embedImage()` for image assets (when the model supports it)
18
- */
19
- type?: "text";
20
- })
21
- | (BaseConfig & {
22
- type: "multimodal";
23
- /**
24
- * Controls how images are translated into AI SDK `embed()` values.
25
- * Different providers use different shapes; this is the escape hatch.
26
- */
27
- image?: {
28
- value?: (input: ImageEmbeddingInput) => unknown;
29
- };
30
- });
12
+ /**
13
+ * Text-only embedding config for the AI SDK provider.
14
+ */
15
+ export type AiEmbeddingConfig = BaseConfig;
31
16
 
32
17
  const DEFAULT_TEXT_MODEL = "openai/text-embedding-3-small";
33
- const DEFAULT_MULTIMODAL_MODEL = "cohere/embed-v4.0";
34
-
35
- const bytesToDataUrl = (bytes: Uint8Array, mediaType: string) => {
36
- const base64 = Buffer.from(bytes).toString("base64");
37
- return `data:${mediaType};base64,${base64}`;
38
- };
39
-
40
- const defaultImageValue = (input: ImageEmbeddingInput) => {
41
- const v =
42
- typeof input.data === "string"
43
- ? input.data
44
- : bytesToDataUrl(input.data, input.mediaType ?? "image/jpeg");
45
- // This matches common AI Gateway multimodal embedding inputs where
46
- // the embedding value is an object containing one or more images.
47
- return { image: [v] };
48
- };
49
18
 
50
19
  export const createAiEmbeddingProvider = (
51
20
  config: AiEmbeddingConfig = {}
52
21
  ): EmbeddingProvider => {
53
- const type = (config as any).type ?? "text";
54
22
  const model =
55
- config.model ??
56
- process.env.AI_GATEWAY_MODEL ??
57
- (type === "multimodal" ? DEFAULT_MULTIMODAL_MODEL : DEFAULT_TEXT_MODEL);
23
+ config.model ?? process.env.AI_GATEWAY_MODEL ?? DEFAULT_TEXT_MODEL;
58
24
  const timeoutMs = config.timeoutMs;
59
25
 
60
26
  return {
@@ -87,37 +53,11 @@ export const createAiEmbeddingProvider = (
87
53
  ...(abortSignal ? { abortSignal } : {}),
88
54
  });
89
55
 
90
- const embeddings = (result as any)?.embeddings as number[][] | undefined;
91
- if (!embeddings) {
56
+ if (!result.embeddings || result.embeddings.length === 0) {
92
57
  throw new Error("Embeddings missing from AI SDK embedMany response");
93
58
  }
94
- return embeddings;
59
+ return result.embeddings;
95
60
  },
96
- ...(type === "multimodal"
97
- ? {
98
- embedImage: async (input: ImageEmbeddingInput) => {
99
- const abortSignal = timeoutMs
100
- ? AbortSignal.timeout(timeoutMs)
101
- : undefined;
102
-
103
- const imageValue =
104
- (config as any)?.image?.value?.(input) ?? defaultImageValue(input);
105
-
106
- const result = await embed({
107
- model,
108
- value: imageValue,
109
- ...(abortSignal ? { abortSignal } : {}),
110
- });
111
-
112
- if (!result.embedding) {
113
- throw new Error("Embedding missing from AI SDK response");
114
- }
115
-
116
- return result.embedding;
117
- },
118
- }
119
- : {}),
120
61
  };
121
62
  };
122
63
 
123
-
@@ -0,0 +1,88 @@
1
+ import { embed, embedMany, type EmbeddingModel } from "ai";
2
+ import type { EmbeddingProvider } from "../core/types";
3
+ import { requireOptional } from "./_shared";
4
+
5
+ /**
6
+ * Azure OpenAI provider module interface.
7
+ */
8
+ interface AzureModule {
9
+ azure: {
10
+ embedding: (model: string) => EmbeddingModel<string>;
11
+ };
12
+ }
13
+
14
+ export type AzureEmbeddingConfig = {
15
+ model?: string;
16
+ timeoutMs?: number;
17
+ dimensions?: number;
18
+ user?: string;
19
+ };
20
+
21
+ const DEFAULT_TEXT_MODEL = "text-embedding-3-small";
22
+
23
+ const buildProviderOptions = (config: AzureEmbeddingConfig) => {
24
+ if (config.dimensions === undefined && config.user === undefined) {
25
+ return undefined;
26
+ }
27
+ return {
28
+ openai: {
29
+ ...(config.dimensions !== undefined ? { dimensions: config.dimensions } : {}),
30
+ ...(config.user ? { user: config.user } : {}),
31
+ },
32
+ };
33
+ };
34
+
35
+ export const createAzureEmbeddingProvider = (
36
+ config: AzureEmbeddingConfig = {}
37
+ ): EmbeddingProvider => {
38
+ const { azure } = requireOptional<AzureModule>({
39
+ id: "@ai-sdk/azure",
40
+ installHint: "bun add @ai-sdk/azure",
41
+ providerName: "azure",
42
+ });
43
+ const model =
44
+ config.model ?? process.env.AZURE_EMBEDDING_MODEL ?? DEFAULT_TEXT_MODEL;
45
+ const timeoutMs = config.timeoutMs;
46
+ const providerOptions = buildProviderOptions(config);
47
+ const embeddingModel = azure.embedding(model);
48
+
49
+ return {
50
+ name: `azure:${model}`,
51
+ dimensions: config.dimensions,
52
+ embed: async ({ text }) => {
53
+ const abortSignal = timeoutMs
54
+ ? AbortSignal.timeout(timeoutMs)
55
+ : undefined;
56
+
57
+ const result = await embed({
58
+ model: embeddingModel,
59
+ value: text,
60
+ ...(providerOptions ? { providerOptions } : {}),
61
+ ...(abortSignal ? { abortSignal } : {}),
62
+ });
63
+
64
+ if (!result.embedding) {
65
+ throw new Error("Embedding missing from Azure OpenAI response");
66
+ }
67
+
68
+ return result.embedding;
69
+ },
70
+ embedMany: async (inputs) => {
71
+ const values = inputs.map((i) => i.text);
72
+ const abortSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
73
+
74
+ const result = await embedMany({
75
+ model: embeddingModel,
76
+ values,
77
+ ...(providerOptions ? { providerOptions } : {}),
78
+ ...(abortSignal ? { abortSignal } : {}),
79
+ });
80
+
81
+ const { embeddings } = result;
82
+ if (!Array.isArray(embeddings)) {
83
+ throw new Error("Embeddings missing from Azure OpenAI embedMany response");
84
+ }
85
+ return embeddings;
86
+ },
87
+ };
88
+ };
@@ -0,0 +1,88 @@
1
+ import { embed, embedMany, type EmbeddingModel } from "ai";
2
+ import type { EmbeddingProvider } from "../core/types";
3
+ import { requireOptional } from "./_shared";
4
+
5
+ /**
6
+ * Amazon Bedrock provider module interface.
7
+ */
8
+ interface BedrockModule {
9
+ bedrock: {
10
+ embedding: (model: string) => EmbeddingModel<string>;
11
+ };
12
+ }
13
+
14
+ export type BedrockEmbeddingConfig = {
15
+ model?: string;
16
+ timeoutMs?: number;
17
+ dimensions?: number;
18
+ normalize?: boolean;
19
+ };
20
+
21
+ const DEFAULT_TEXT_MODEL = "amazon.titan-embed-text-v2:0";
22
+
23
+ const buildProviderOptions = (config: BedrockEmbeddingConfig) => {
24
+ if (config.dimensions === undefined && config.normalize === undefined) {
25
+ return undefined;
26
+ }
27
+ return {
28
+ bedrock: {
29
+ ...(config.dimensions !== undefined ? { dimensions: config.dimensions } : {}),
30
+ ...(config.normalize !== undefined ? { normalize: config.normalize } : {}),
31
+ },
32
+ };
33
+ };
34
+
35
+ export const createBedrockEmbeddingProvider = (
36
+ config: BedrockEmbeddingConfig = {}
37
+ ): EmbeddingProvider => {
38
+ const { bedrock } = requireOptional<BedrockModule>({
39
+ id: "@ai-sdk/amazon-bedrock",
40
+ installHint: "bun add @ai-sdk/amazon-bedrock",
41
+ providerName: "bedrock",
42
+ });
43
+ const model =
44
+ config.model ?? process.env.BEDROCK_EMBEDDING_MODEL ?? DEFAULT_TEXT_MODEL;
45
+ const timeoutMs = config.timeoutMs;
46
+ const providerOptions = buildProviderOptions(config);
47
+ const embeddingModel = bedrock.embedding(model);
48
+
49
+ return {
50
+ name: `bedrock:${model}`,
51
+ dimensions: config.dimensions,
52
+ embed: async ({ text }) => {
53
+ const abortSignal = timeoutMs
54
+ ? AbortSignal.timeout(timeoutMs)
55
+ : undefined;
56
+
57
+ const result = await embed({
58
+ model: embeddingModel,
59
+ value: text,
60
+ ...(providerOptions ? { providerOptions } : {}),
61
+ ...(abortSignal ? { abortSignal } : {}),
62
+ });
63
+
64
+ if (!result.embedding) {
65
+ throw new Error("Embedding missing from Bedrock response");
66
+ }
67
+
68
+ return result.embedding;
69
+ },
70
+ embedMany: async (inputs) => {
71
+ const values = inputs.map((i) => i.text);
72
+ const abortSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
73
+
74
+ const result = await embedMany({
75
+ model: embeddingModel,
76
+ values,
77
+ ...(providerOptions ? { providerOptions } : {}),
78
+ ...(abortSignal ? { abortSignal } : {}),
79
+ });
80
+
81
+ const { embeddings } = result;
82
+ if (!Array.isArray(embeddings)) {
83
+ throw new Error("Embeddings missing from Bedrock embedMany response");
84
+ }
85
+ return embeddings;
86
+ },
87
+ };
88
+ };