@ai-sdk/amazon-bedrock 4.0.24 → 4.0.26
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/CHANGELOG.md +16 -0
- package/dist/anthropic/index.js +1 -1
- package/dist/anthropic/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/08-amazon-bedrock.mdx +1453 -0
- package/package.json +11 -6
- package/src/__fixtures__/bedrock-json-only-text-first.1.chunks.txt +7 -0
- package/src/__fixtures__/bedrock-json-other-tool.1.chunks.txt +6 -0
- package/src/__fixtures__/bedrock-json-other-tool.1.json +24 -0
- package/src/__fixtures__/bedrock-json-tool-text-then-weather-then-json.1.chunks.txt +12 -0
- package/src/__fixtures__/bedrock-json-tool-with-answer.1.json +29 -0
- package/src/__fixtures__/bedrock-json-tool.1.chunks.txt +4 -0
- package/src/__fixtures__/bedrock-json-tool.1.json +35 -0
- package/src/__fixtures__/bedrock-json-tool.2.chunks.txt +6 -0
- package/src/__fixtures__/bedrock-json-tool.2.json +28 -0
- package/src/__fixtures__/bedrock-json-tool.3.chunks.txt +7 -0
- package/src/__fixtures__/bedrock-json-tool.3.json +36 -0
- package/src/__fixtures__/bedrock-json-with-tool.1.chunks.txt +9 -0
- package/src/__fixtures__/bedrock-json-with-tool.1.json +41 -0
- package/src/__fixtures__/bedrock-json-with-tools.1.chunks.txt +12 -0
- package/src/__fixtures__/bedrock-json-with-tools.1.json +50 -0
- package/src/__fixtures__/bedrock-tool-call.1.chunks.txt +6 -0
- package/src/__fixtures__/bedrock-tool-call.1.json +24 -0
- package/src/__fixtures__/bedrock-tool-no-args.chunks.txt +8 -0
- package/src/__fixtures__/bedrock-tool-no-args.json +25 -0
- package/src/anthropic/bedrock-anthropic-fetch.test.ts +344 -0
- package/src/anthropic/bedrock-anthropic-fetch.ts +62 -0
- package/src/anthropic/bedrock-anthropic-options.ts +28 -0
- package/src/anthropic/bedrock-anthropic-provider.test.ts +456 -0
- package/src/anthropic/bedrock-anthropic-provider.ts +357 -0
- package/src/anthropic/index.ts +9 -0
- package/src/bedrock-api-types.ts +195 -0
- package/src/bedrock-chat-language-model.test.ts +4569 -0
- package/src/bedrock-chat-language-model.ts +1019 -0
- package/src/bedrock-chat-options.ts +114 -0
- package/src/bedrock-embedding-model.test.ts +148 -0
- package/src/bedrock-embedding-model.ts +104 -0
- package/src/bedrock-embedding-options.ts +24 -0
- package/src/bedrock-error.ts +6 -0
- package/src/bedrock-event-stream-decoder.ts +59 -0
- package/src/bedrock-event-stream-response-handler.test.ts +233 -0
- package/src/bedrock-event-stream-response-handler.ts +57 -0
- package/src/bedrock-image-model.test.ts +866 -0
- package/src/bedrock-image-model.ts +297 -0
- package/src/bedrock-image-settings.ts +6 -0
- package/src/bedrock-prepare-tools.ts +190 -0
- package/src/bedrock-provider.test.ts +457 -0
- package/src/bedrock-provider.ts +351 -0
- package/src/bedrock-sigv4-fetch.test.ts +675 -0
- package/src/bedrock-sigv4-fetch.ts +138 -0
- package/src/convert-bedrock-usage.test.ts +207 -0
- package/src/convert-bedrock-usage.ts +50 -0
- package/src/convert-to-bedrock-chat-messages.test.ts +1175 -0
- package/src/convert-to-bedrock-chat-messages.ts +452 -0
- package/src/index.ts +10 -0
- package/src/inject-fetch-headers.test.ts +135 -0
- package/src/inject-fetch-headers.ts +32 -0
- package/src/map-bedrock-finish-reason.ts +22 -0
- package/src/normalize-tool-call-id.test.ts +72 -0
- package/src/normalize-tool-call-id.ts +36 -0
- package/src/reranking/__fixtures__/bedrock-reranking.1.json +12 -0
- package/src/reranking/bedrock-reranking-api.ts +44 -0
- package/src/reranking/bedrock-reranking-model.test.ts +299 -0
- package/src/reranking/bedrock-reranking-model.ts +115 -0
- package/src/reranking/bedrock-reranking-options.ts +36 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
// https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html
|
|
4
|
+
export type BedrockChatModelId =
|
|
5
|
+
| 'amazon.titan-tg1-large'
|
|
6
|
+
| 'amazon.titan-text-express-v1'
|
|
7
|
+
| 'anthropic.claude-v2'
|
|
8
|
+
| 'anthropic.claude-v2:1'
|
|
9
|
+
| 'anthropic.claude-instant-v1'
|
|
10
|
+
| 'anthropic.claude-haiku-4-5-20251001-v1:0'
|
|
11
|
+
| 'anthropic.claude-sonnet-4-20250514-v1:0'
|
|
12
|
+
| 'anthropic.claude-sonnet-4-5-20250929-v1:0'
|
|
13
|
+
| 'anthropic.claude-opus-4-20250514-v1:0'
|
|
14
|
+
| 'anthropic.claude-opus-4-1-20250805-v1:0'
|
|
15
|
+
| 'anthropic.claude-3-7-sonnet-20250219-v1:0'
|
|
16
|
+
| 'anthropic.claude-3-5-sonnet-20240620-v1:0'
|
|
17
|
+
| 'anthropic.claude-3-5-sonnet-20241022-v2:0'
|
|
18
|
+
| 'anthropic.claude-3-5-haiku-20241022-v1:0'
|
|
19
|
+
| 'anthropic.claude-3-sonnet-20240229-v1:0'
|
|
20
|
+
| 'anthropic.claude-3-haiku-20240307-v1:0'
|
|
21
|
+
| 'anthropic.claude-3-opus-20240229-v1:0'
|
|
22
|
+
| 'cohere.command-text-v14'
|
|
23
|
+
| 'cohere.command-light-text-v14'
|
|
24
|
+
| 'cohere.command-r-v1:0'
|
|
25
|
+
| 'cohere.command-r-plus-v1:0'
|
|
26
|
+
| 'meta.llama3-70b-instruct-v1:0'
|
|
27
|
+
| 'meta.llama3-8b-instruct-v1:0'
|
|
28
|
+
| 'meta.llama3-1-405b-instruct-v1:0'
|
|
29
|
+
| 'meta.llama3-1-70b-instruct-v1:0'
|
|
30
|
+
| 'meta.llama3-1-8b-instruct-v1:0'
|
|
31
|
+
| 'meta.llama3-2-11b-instruct-v1:0'
|
|
32
|
+
| 'meta.llama3-2-1b-instruct-v1:0'
|
|
33
|
+
| 'meta.llama3-2-3b-instruct-v1:0'
|
|
34
|
+
| 'meta.llama3-2-90b-instruct-v1:0'
|
|
35
|
+
| 'mistral.mistral-7b-instruct-v0:2'
|
|
36
|
+
| 'mistral.mixtral-8x7b-instruct-v0:1'
|
|
37
|
+
| 'mistral.mistral-large-2402-v1:0'
|
|
38
|
+
| 'mistral.mistral-small-2402-v1:0'
|
|
39
|
+
| 'openai.gpt-oss-120b-1:0'
|
|
40
|
+
| 'openai.gpt-oss-20b-1:0'
|
|
41
|
+
| 'amazon.titan-text-express-v1'
|
|
42
|
+
| 'amazon.titan-text-lite-v1'
|
|
43
|
+
| 'us.amazon.nova-premier-v1:0'
|
|
44
|
+
| 'us.amazon.nova-pro-v1:0'
|
|
45
|
+
| 'us.amazon.nova-micro-v1:0'
|
|
46
|
+
| 'us.amazon.nova-lite-v1:0'
|
|
47
|
+
| 'us.anthropic.claude-3-sonnet-20240229-v1:0'
|
|
48
|
+
| 'us.anthropic.claude-3-opus-20240229-v1:0'
|
|
49
|
+
| 'us.anthropic.claude-3-haiku-20240307-v1:0'
|
|
50
|
+
| 'us.anthropic.claude-3-5-sonnet-20240620-v1:0'
|
|
51
|
+
| 'us.anthropic.claude-3-5-haiku-20241022-v1:0'
|
|
52
|
+
| 'us.anthropic.claude-3-5-sonnet-20241022-v2:0'
|
|
53
|
+
| 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'
|
|
54
|
+
| 'us.anthropic.claude-sonnet-4-20250514-v1:0'
|
|
55
|
+
| 'us.anthropic.claude-sonnet-4-5-20250929-v1:0'
|
|
56
|
+
| 'us.anthropic.claude-opus-4-20250514-v1:0'
|
|
57
|
+
| 'us.anthropic.claude-opus-4-1-20250805-v1:0'
|
|
58
|
+
| 'us.meta.llama3-2-11b-instruct-v1:0'
|
|
59
|
+
| 'us.meta.llama3-2-3b-instruct-v1:0'
|
|
60
|
+
| 'us.meta.llama3-2-90b-instruct-v1:0'
|
|
61
|
+
| 'us.meta.llama3-2-1b-instruct-v1:0'
|
|
62
|
+
| 'us.meta.llama3-1-8b-instruct-v1:0'
|
|
63
|
+
| 'us.meta.llama3-1-70b-instruct-v1:0'
|
|
64
|
+
| 'us.meta.llama3-3-70b-instruct-v1:0'
|
|
65
|
+
| 'us.deepseek.r1-v1:0'
|
|
66
|
+
| 'us.mistral.pixtral-large-2502-v1:0'
|
|
67
|
+
| 'us.meta.llama4-scout-17b-instruct-v1:0'
|
|
68
|
+
| 'us.meta.llama4-maverick-17b-instruct-v1:0'
|
|
69
|
+
| (string & {});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Bedrock file part provider options for document-specific features.
|
|
73
|
+
* These options apply to individual file parts (documents).
|
|
74
|
+
*/
|
|
75
|
+
export const bedrockFilePartProviderOptions = z.object({
|
|
76
|
+
/**
|
|
77
|
+
* Citation configuration for this document.
|
|
78
|
+
* When enabled, this document will generate citations in the response.
|
|
79
|
+
*/
|
|
80
|
+
citations: z
|
|
81
|
+
.object({
|
|
82
|
+
/**
|
|
83
|
+
* Enable citations for this document
|
|
84
|
+
*/
|
|
85
|
+
enabled: z.boolean(),
|
|
86
|
+
})
|
|
87
|
+
.optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export type BedrockFilePartProviderOptions = z.infer<
|
|
91
|
+
typeof bedrockFilePartProviderOptions
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
export const bedrockProviderOptions = z.object({
|
|
95
|
+
/**
|
|
96
|
+
* Additional inference parameters that the model supports,
|
|
97
|
+
* beyond the base set of inference parameters that Converse
|
|
98
|
+
* supports in the inferenceConfig field
|
|
99
|
+
*/
|
|
100
|
+
additionalModelRequestFields: z.record(z.string(), z.any()).optional(),
|
|
101
|
+
reasoningConfig: z
|
|
102
|
+
.object({
|
|
103
|
+
type: z.union([z.literal('enabled'), z.literal('disabled')]).optional(),
|
|
104
|
+
budgetTokens: z.number().optional(),
|
|
105
|
+
maxReasoningEffort: z.enum(['low', 'medium', 'high']).optional(),
|
|
106
|
+
})
|
|
107
|
+
.optional(),
|
|
108
|
+
/**
|
|
109
|
+
* Anthropic beta features to enable
|
|
110
|
+
*/
|
|
111
|
+
anthropicBeta: z.array(z.string()).optional(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export type BedrockProviderOptions = z.infer<typeof bedrockProviderOptions>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
|
|
2
|
+
import { BedrockEmbeddingModel } from './bedrock-embedding-model';
|
|
3
|
+
import { injectFetchHeaders } from './inject-fetch-headers';
|
|
4
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const mockEmbeddings = [
|
|
7
|
+
[-0.09, 0.05, -0.02, 0.01, 0.04],
|
|
8
|
+
[-0.08, 0.06, -0.03, 0.02, 0.03],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' });
|
|
12
|
+
|
|
13
|
+
const testValues = ['sunny day at the beach', 'rainy day in the city'];
|
|
14
|
+
|
|
15
|
+
const embedUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encodeURIComponent(
|
|
16
|
+
'amazon.titan-embed-text-v2:0',
|
|
17
|
+
)}/invoke`;
|
|
18
|
+
|
|
19
|
+
describe('doEmbed', () => {
|
|
20
|
+
const mockConfigHeaders = {
|
|
21
|
+
'config-header': 'config-value',
|
|
22
|
+
'shared-header': 'config-shared',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const server = createTestServer({
|
|
26
|
+
[embedUrl]: {
|
|
27
|
+
response: {
|
|
28
|
+
type: 'binary',
|
|
29
|
+
headers: {
|
|
30
|
+
'content-type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: Buffer.from(
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
embedding: mockEmbeddings[0],
|
|
35
|
+
inputTextTokenCount: 8,
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const model = new BedrockEmbeddingModel('amazon.titan-embed-text-v2:0', {
|
|
43
|
+
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
|
|
44
|
+
headers: mockConfigHeaders,
|
|
45
|
+
fetch: fakeFetchWithAuth,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let callCount = 0;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
callCount = 0;
|
|
52
|
+
server.urls[embedUrl].response = {
|
|
53
|
+
type: 'binary',
|
|
54
|
+
headers: {
|
|
55
|
+
'content-type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: Buffer.from(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
embedding: mockEmbeddings[0],
|
|
60
|
+
inputTextTokenCount: 8,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle single input value and return embeddings', async () => {
|
|
67
|
+
const { embeddings } = await model.doEmbed({
|
|
68
|
+
values: [testValues[0]],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(embeddings.length).toBe(1);
|
|
72
|
+
expect(embeddings[0]).toStrictEqual(mockEmbeddings[0]);
|
|
73
|
+
|
|
74
|
+
const body = await server.calls[0].requestBodyJson;
|
|
75
|
+
expect(body).toEqual({
|
|
76
|
+
inputText: testValues[0],
|
|
77
|
+
dimensions: undefined,
|
|
78
|
+
normalize: undefined,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle single input value and extract usage', async () => {
|
|
83
|
+
const { usage } = await model.doEmbed({
|
|
84
|
+
values: [testValues[0]],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(usage?.tokens).toStrictEqual(8);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should properly combine headers from all sources', async () => {
|
|
91
|
+
const optionsHeaders = {
|
|
92
|
+
'options-header': 'options-value',
|
|
93
|
+
'shared-header': 'options-shared',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const modelWithHeaders = new BedrockEmbeddingModel(
|
|
97
|
+
'amazon.titan-embed-text-v2:0',
|
|
98
|
+
{
|
|
99
|
+
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
|
|
100
|
+
headers: {
|
|
101
|
+
'model-header': 'model-value',
|
|
102
|
+
'shared-header': 'model-shared',
|
|
103
|
+
},
|
|
104
|
+
fetch: injectFetchHeaders({
|
|
105
|
+
'signed-header': 'signed-value',
|
|
106
|
+
authorization: 'AWS4-HMAC-SHA256...',
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await modelWithHeaders.doEmbed({
|
|
112
|
+
values: [testValues[0]],
|
|
113
|
+
headers: optionsHeaders,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const requestHeaders = server.calls[0].requestHeaders;
|
|
117
|
+
expect(requestHeaders['options-header']).toBe('options-value');
|
|
118
|
+
expect(requestHeaders['model-header']).toBe('model-value');
|
|
119
|
+
expect(requestHeaders['signed-header']).toBe('signed-value');
|
|
120
|
+
expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
|
|
121
|
+
expect(requestHeaders['shared-header']).toBe('options-shared');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should work with partial headers', async () => {
|
|
125
|
+
const modelWithPartialHeaders = new BedrockEmbeddingModel(
|
|
126
|
+
'amazon.titan-embed-text-v2:0',
|
|
127
|
+
{
|
|
128
|
+
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
|
|
129
|
+
headers: {
|
|
130
|
+
'model-header': 'model-value',
|
|
131
|
+
},
|
|
132
|
+
fetch: injectFetchHeaders({
|
|
133
|
+
'signed-header': 'signed-value',
|
|
134
|
+
authorization: 'AWS4-HMAC-SHA256...',
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await modelWithPartialHeaders.doEmbed({
|
|
140
|
+
values: [testValues[0]],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const requestHeaders = server.calls[0].requestHeaders;
|
|
144
|
+
expect(requestHeaders['model-header']).toBe('model-value');
|
|
145
|
+
expect(requestHeaders['signed-header']).toBe('signed-value');
|
|
146
|
+
expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EmbeddingModelV3,
|
|
3
|
+
TooManyEmbeddingValuesForCallError,
|
|
4
|
+
} from '@ai-sdk/provider';
|
|
5
|
+
import {
|
|
6
|
+
FetchFunction,
|
|
7
|
+
Resolvable,
|
|
8
|
+
combineHeaders,
|
|
9
|
+
createJsonErrorResponseHandler,
|
|
10
|
+
createJsonResponseHandler,
|
|
11
|
+
parseProviderOptions,
|
|
12
|
+
postJsonToApi,
|
|
13
|
+
resolve,
|
|
14
|
+
} from '@ai-sdk/provider-utils';
|
|
15
|
+
import {
|
|
16
|
+
BedrockEmbeddingModelId,
|
|
17
|
+
bedrockEmbeddingProviderOptions,
|
|
18
|
+
} from './bedrock-embedding-options';
|
|
19
|
+
import { BedrockErrorSchema } from './bedrock-error';
|
|
20
|
+
import { z } from 'zod/v4';
|
|
21
|
+
|
|
22
|
+
type BedrockEmbeddingConfig = {
|
|
23
|
+
baseUrl: () => string;
|
|
24
|
+
headers: Resolvable<Record<string, string | undefined>>;
|
|
25
|
+
fetch?: FetchFunction;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type DoEmbedResponse = Awaited<ReturnType<EmbeddingModelV3['doEmbed']>>;
|
|
29
|
+
|
|
30
|
+
export class BedrockEmbeddingModel implements EmbeddingModelV3 {
|
|
31
|
+
readonly specificationVersion = 'v3';
|
|
32
|
+
readonly provider = 'amazon-bedrock';
|
|
33
|
+
readonly maxEmbeddingsPerCall = 1;
|
|
34
|
+
readonly supportsParallelCalls = true;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
readonly modelId: BedrockEmbeddingModelId,
|
|
38
|
+
private readonly config: BedrockEmbeddingConfig,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
private getUrl(modelId: string): string {
|
|
42
|
+
const encodedModelId = encodeURIComponent(modelId);
|
|
43
|
+
return `${this.config.baseUrl()}/model/${encodedModelId}/invoke`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async doEmbed({
|
|
47
|
+
values,
|
|
48
|
+
headers,
|
|
49
|
+
abortSignal,
|
|
50
|
+
providerOptions,
|
|
51
|
+
}: Parameters<EmbeddingModelV3['doEmbed']>[0]): Promise<DoEmbedResponse> {
|
|
52
|
+
if (values.length > this.maxEmbeddingsPerCall) {
|
|
53
|
+
throw new TooManyEmbeddingValuesForCallError({
|
|
54
|
+
provider: this.provider,
|
|
55
|
+
modelId: this.modelId,
|
|
56
|
+
maxEmbeddingsPerCall: this.maxEmbeddingsPerCall,
|
|
57
|
+
values,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Parse provider options
|
|
62
|
+
const bedrockOptions =
|
|
63
|
+
(await parseProviderOptions({
|
|
64
|
+
provider: 'bedrock',
|
|
65
|
+
providerOptions,
|
|
66
|
+
schema: bedrockEmbeddingProviderOptions,
|
|
67
|
+
})) ?? {};
|
|
68
|
+
|
|
69
|
+
// https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html
|
|
70
|
+
const args = {
|
|
71
|
+
inputText: values[0],
|
|
72
|
+
dimensions: bedrockOptions.dimensions,
|
|
73
|
+
normalize: bedrockOptions.normalize,
|
|
74
|
+
};
|
|
75
|
+
const url = this.getUrl(this.modelId);
|
|
76
|
+
const { value: response } = await postJsonToApi({
|
|
77
|
+
url,
|
|
78
|
+
headers: await resolve(
|
|
79
|
+
combineHeaders(await resolve(this.config.headers), headers),
|
|
80
|
+
),
|
|
81
|
+
body: args,
|
|
82
|
+
failedResponseHandler: createJsonErrorResponseHandler({
|
|
83
|
+
errorSchema: BedrockErrorSchema,
|
|
84
|
+
errorToMessage: error => `${error.type}: ${error.message}`,
|
|
85
|
+
}),
|
|
86
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
87
|
+
BedrockEmbeddingResponseSchema,
|
|
88
|
+
),
|
|
89
|
+
fetch: this.config.fetch,
|
|
90
|
+
abortSignal,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
warnings: [],
|
|
95
|
+
embeddings: [response.embedding],
|
|
96
|
+
usage: { tokens: response.inputTextTokenCount },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const BedrockEmbeddingResponseSchema = z.object({
|
|
102
|
+
embedding: z.array(z.number()),
|
|
103
|
+
inputTextTokenCount: z.number(),
|
|
104
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
export type BedrockEmbeddingModelId =
|
|
4
|
+
| 'amazon.titan-embed-text-v1'
|
|
5
|
+
| 'amazon.titan-embed-text-v2:0'
|
|
6
|
+
| 'cohere.embed-english-v3'
|
|
7
|
+
| 'cohere.embed-multilingual-v3'
|
|
8
|
+
| (string & {});
|
|
9
|
+
|
|
10
|
+
export const bedrockEmbeddingProviderOptions = z.object({
|
|
11
|
+
/**
|
|
12
|
+
The number of dimensions the resulting output embeddings should have (defaults to 1024).
|
|
13
|
+
Only supported in amazon.titan-embed-text-v2:0.
|
|
14
|
+
*/
|
|
15
|
+
dimensions: z
|
|
16
|
+
.union([z.literal(1024), z.literal(512), z.literal(256)])
|
|
17
|
+
.optional(),
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
Flag indicating whether or not to normalize the output embeddings. Defaults to true
|
|
21
|
+
Only supported in amazon.titan-embed-text-v2:0.
|
|
22
|
+
*/
|
|
23
|
+
normalize: z.boolean().optional(),
|
|
24
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EventStreamCodec } from '@smithy/eventstream-codec';
|
|
2
|
+
import { toUtf8, fromUtf8 } from '@smithy/util-utf8';
|
|
3
|
+
|
|
4
|
+
export interface DecodedEvent {
|
|
5
|
+
messageType: string;
|
|
6
|
+
eventType: string;
|
|
7
|
+
data: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createBedrockEventStreamDecoder<T>(
|
|
11
|
+
body: ReadableStream<Uint8Array>,
|
|
12
|
+
processEvent: (
|
|
13
|
+
event: DecodedEvent,
|
|
14
|
+
controller: TransformStreamDefaultController<T>,
|
|
15
|
+
) => void | Promise<void>,
|
|
16
|
+
): ReadableStream<T> {
|
|
17
|
+
const codec = new EventStreamCodec(toUtf8, fromUtf8);
|
|
18
|
+
let buffer = new Uint8Array(0);
|
|
19
|
+
const textDecoder = new TextDecoder();
|
|
20
|
+
|
|
21
|
+
return body.pipeThrough(
|
|
22
|
+
new TransformStream<Uint8Array, T>({
|
|
23
|
+
async transform(chunk, controller) {
|
|
24
|
+
const newBuffer = new Uint8Array(buffer.length + chunk.length);
|
|
25
|
+
newBuffer.set(buffer);
|
|
26
|
+
newBuffer.set(chunk, buffer.length);
|
|
27
|
+
buffer = newBuffer;
|
|
28
|
+
|
|
29
|
+
while (buffer.length >= 4) {
|
|
30
|
+
const totalLength = new DataView(
|
|
31
|
+
buffer.buffer,
|
|
32
|
+
buffer.byteOffset,
|
|
33
|
+
buffer.byteLength,
|
|
34
|
+
).getUint32(0, false);
|
|
35
|
+
|
|
36
|
+
if (buffer.length < totalLength) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const subView = buffer.subarray(0, totalLength);
|
|
42
|
+
const decoded = codec.decode(subView);
|
|
43
|
+
|
|
44
|
+
buffer = buffer.slice(totalLength);
|
|
45
|
+
|
|
46
|
+
const messageType = decoded.headers[':message-type']
|
|
47
|
+
?.value as string;
|
|
48
|
+
const eventType = decoded.headers[':event-type']?.value as string;
|
|
49
|
+
const data = textDecoder.decode(decoded.body);
|
|
50
|
+
|
|
51
|
+
await processEvent({ messageType, eventType, data }, controller);
|
|
52
|
+
} catch {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { EmptyResponseBodyError } from '@ai-sdk/provider';
|
|
2
|
+
import { createBedrockEventStreamResponseHandler } from './bedrock-event-stream-response-handler';
|
|
3
|
+
import { EventStreamCodec } from '@smithy/eventstream-codec';
|
|
4
|
+
import { z } from 'zod/v4';
|
|
5
|
+
import { describe, it, expect, vi, MockInstance } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// Helper that constructs a properly framed message.
|
|
8
|
+
// The first 4 bytes will contain the frame total length (big-endian).
|
|
9
|
+
const createFrame = (payload: Uint8Array): Uint8Array => {
|
|
10
|
+
const totalLength = 4 + payload.length;
|
|
11
|
+
const frame = new Uint8Array(totalLength);
|
|
12
|
+
new DataView(frame.buffer).setUint32(0, totalLength, false);
|
|
13
|
+
frame.set(payload, 4);
|
|
14
|
+
return frame;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Mock EventStreamCodec
|
|
18
|
+
vi.mock('@smithy/eventstream-codec', () => ({
|
|
19
|
+
EventStreamCodec: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('createEventSourceResponseHandler', () => {
|
|
23
|
+
// Define a sample schema for testing
|
|
24
|
+
const testSchema = z.object({
|
|
25
|
+
chunk: z.object({
|
|
26
|
+
content: z.string(),
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws EmptyResponseBodyError when response body is null', async () => {
|
|
31
|
+
const response = new Response(null);
|
|
32
|
+
const handler = createBedrockEventStreamResponseHandler(testSchema);
|
|
33
|
+
|
|
34
|
+
await expect(
|
|
35
|
+
handler({
|
|
36
|
+
response,
|
|
37
|
+
url: 'test-url',
|
|
38
|
+
requestBodyValues: {},
|
|
39
|
+
}),
|
|
40
|
+
).rejects.toThrow(EmptyResponseBodyError);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('successfully processes valid event stream data', async () => {
|
|
44
|
+
// Prepare the message we wish to simulate.
|
|
45
|
+
// Our decoded message will contain headers and a body that is valid JSON.
|
|
46
|
+
const message = {
|
|
47
|
+
headers: {
|
|
48
|
+
':message-type': { value: 'event' },
|
|
49
|
+
':event-type': { value: 'chunk' },
|
|
50
|
+
},
|
|
51
|
+
body: new TextEncoder().encode(
|
|
52
|
+
JSON.stringify({ content: 'test message' }),
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Create a frame that properly encapsulates the message.
|
|
57
|
+
const dummyPayload = new Uint8Array([1, 2, 3, 4]); // arbitrary payload that makes the length check pass
|
|
58
|
+
const frame = createFrame(dummyPayload);
|
|
59
|
+
|
|
60
|
+
const mockDecode = vi.fn().mockReturnValue(message);
|
|
61
|
+
(EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({
|
|
62
|
+
decode: mockDecode,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Create a stream that enqueues the complete frame.
|
|
66
|
+
const stream = new ReadableStream({
|
|
67
|
+
start(controller) {
|
|
68
|
+
controller.enqueue(frame);
|
|
69
|
+
controller.close();
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const response = new Response(stream);
|
|
74
|
+
const handler = createBedrockEventStreamResponseHandler(testSchema);
|
|
75
|
+
const result = await handler({
|
|
76
|
+
response,
|
|
77
|
+
url: 'test-url',
|
|
78
|
+
requestBodyValues: {},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const reader = result.value.getReader();
|
|
82
|
+
const { done, value } = await reader.read();
|
|
83
|
+
|
|
84
|
+
expect(done).toBe(false);
|
|
85
|
+
expect(value).toEqual({
|
|
86
|
+
success: true,
|
|
87
|
+
value: { chunk: { content: 'test message' } },
|
|
88
|
+
rawValue: { chunk: { content: 'test message' } },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles invalid JSON data', async () => {
|
|
93
|
+
// Our mock decode returns a body that is not valid JSON.
|
|
94
|
+
const message = {
|
|
95
|
+
headers: {
|
|
96
|
+
':message-type': { value: 'event' },
|
|
97
|
+
':event-type': { value: 'chunk' },
|
|
98
|
+
},
|
|
99
|
+
body: new TextEncoder().encode('invalid json'),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const dummyPayload = new Uint8Array([5, 6, 7, 8]);
|
|
103
|
+
const frame = createFrame(dummyPayload);
|
|
104
|
+
|
|
105
|
+
const mockDecode = vi.fn().mockReturnValue(message);
|
|
106
|
+
(EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({
|
|
107
|
+
decode: mockDecode,
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const stream = new ReadableStream({
|
|
111
|
+
start(controller) {
|
|
112
|
+
controller.enqueue(frame);
|
|
113
|
+
controller.close();
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const response = new Response(stream);
|
|
118
|
+
const handler = createBedrockEventStreamResponseHandler(testSchema);
|
|
119
|
+
const result = await handler({
|
|
120
|
+
response,
|
|
121
|
+
url: 'test-url',
|
|
122
|
+
requestBodyValues: {},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const reader = result.value.getReader();
|
|
126
|
+
const { done, value } = await reader.read();
|
|
127
|
+
|
|
128
|
+
expect(done).toBe(false);
|
|
129
|
+
// When JSON is invalid, safeParseJSON returns a result with success: false.
|
|
130
|
+
expect(value?.success).toBe(false);
|
|
131
|
+
expect((value as { success: false; error: Error }).error).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('handles schema validation failures', async () => {
|
|
135
|
+
// The decoded message returns valid JSON but that does not meet our schema.
|
|
136
|
+
const message = {
|
|
137
|
+
headers: {
|
|
138
|
+
':message-type': { value: 'event' },
|
|
139
|
+
':event-type': { value: 'chunk' },
|
|
140
|
+
},
|
|
141
|
+
body: new TextEncoder().encode(JSON.stringify({ invalid: 'data' })),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const dummyPayload = new Uint8Array([9, 10, 11, 12]);
|
|
145
|
+
const frame = createFrame(dummyPayload);
|
|
146
|
+
|
|
147
|
+
const mockDecode = vi.fn().mockReturnValue(message);
|
|
148
|
+
(EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({
|
|
149
|
+
decode: mockDecode,
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
const stream = new ReadableStream({
|
|
153
|
+
start(controller) {
|
|
154
|
+
controller.enqueue(frame);
|
|
155
|
+
controller.close();
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const response = new Response(stream);
|
|
160
|
+
const handler = createBedrockEventStreamResponseHandler(testSchema);
|
|
161
|
+
const result = await handler({
|
|
162
|
+
response,
|
|
163
|
+
url: 'test-url',
|
|
164
|
+
requestBodyValues: {},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const reader = result.value.getReader();
|
|
168
|
+
const { done, value } = await reader.read();
|
|
169
|
+
|
|
170
|
+
expect(done).toBe(false);
|
|
171
|
+
// The schema does not match so safeParseJSON with the schema should yield success: false.
|
|
172
|
+
expect(value?.success).toBe(false);
|
|
173
|
+
expect((value as { success: false; error: Error }).error).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('handles partial messages correctly', async () => {
|
|
177
|
+
// In this test, we simulate a partial message followed by a complete one.
|
|
178
|
+
// The first invocation of decode will throw an error (simulated incomplete message),
|
|
179
|
+
// and the subsequent invocation returns a valid event.
|
|
180
|
+
const message = {
|
|
181
|
+
headers: {
|
|
182
|
+
':message-type': { value: 'event' },
|
|
183
|
+
':event-type': { value: 'chunk' },
|
|
184
|
+
},
|
|
185
|
+
body: new TextEncoder().encode(
|
|
186
|
+
JSON.stringify({ content: 'complete message' }),
|
|
187
|
+
),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const dummyPayload1 = new Uint8Array([13, 14]); // too short, part of a frame
|
|
191
|
+
const frame1 = createFrame(dummyPayload1);
|
|
192
|
+
const dummyPayload2 = new Uint8Array([15, 16, 17, 18]);
|
|
193
|
+
const frame2 = createFrame(dummyPayload2);
|
|
194
|
+
|
|
195
|
+
const mockDecode = vi
|
|
196
|
+
.fn()
|
|
197
|
+
.mockImplementationOnce(() => {
|
|
198
|
+
throw new Error('Incomplete data');
|
|
199
|
+
})
|
|
200
|
+
.mockReturnValue(message);
|
|
201
|
+
(EventStreamCodec as unknown as MockInstance).mockImplementation(() => ({
|
|
202
|
+
decode: mockDecode,
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
const stream = new ReadableStream({
|
|
206
|
+
start(controller) {
|
|
207
|
+
// Send first, incomplete frame (decode will throw error).
|
|
208
|
+
controller.enqueue(frame1);
|
|
209
|
+
// Then send a proper frame.
|
|
210
|
+
controller.enqueue(frame2);
|
|
211
|
+
controller.close();
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const response = new Response(stream);
|
|
216
|
+
const handler = createBedrockEventStreamResponseHandler(testSchema);
|
|
217
|
+
const result = await handler({
|
|
218
|
+
response,
|
|
219
|
+
url: 'test-url',
|
|
220
|
+
requestBodyValues: {},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const reader = result.value.getReader();
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
|
|
226
|
+
expect(done).toBe(false);
|
|
227
|
+
expect(value).toEqual({
|
|
228
|
+
success: true,
|
|
229
|
+
value: { chunk: { content: 'complete message' } },
|
|
230
|
+
rawValue: { chunk: { content: 'complete message' } },
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|