Skip to content

Commit 6ece44c

Browse files
fix(amazon-bedrock): support Cohere embedding models (#11213)
## Background Cohere embedding model IDs (e.g. `cohere.embed-english-v3`, `cohere.embed-multilingual-v3`) are listed as supported by `@ai-sdk/amazon-bedrock`, but embedding requests fail at runtime because Cohere-on-Bedrock requires a different request schema than Titan (notably a required `input_type`). This prevents Cohere embedding models from being usable through the Bedrock provider and triggers a 400 “Malformed input request” error. Fixes [#5055](#5055). ## Summary - Updated `BedrockEmbeddingModel` to detect `cohere.embed-*` model IDs and: - Send Cohere-compatible request payload including required `input_type` (defaulting to `search_query`) and optional `truncate`. - Parse Cohere-style embedding responses (`{ embeddings: number[][] }`) in addition to Titan-style responses. - Extended `bedrockEmbeddingProviderOptions` with Cohere-specific options: - `inputType` (`search_document | search_query | classification | clustering`) - `truncate` (`NONE | START | END`) - Added/updated unit tests for Cohere embedding request/response handling. - Updated Bedrock provider docs to mention the required Cohere `inputType`. - Added a **patch changeset** for `@ai-sdk/amazon-bedrock`. ## Manual Verification - Verified end-to-end by running `embed()` against Amazon Bedrock using `cohere.embed-english-v3` and confirming the API call succeeds and returns a numeric embedding vector (previously returned a 400 validation error complaining about missing `input_type`). ## Checklist - [x] Tests have been added / updated (for bug fixes / features) - [x] Documentation has been added / updated (for bug fixes / features) - [x] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) - [x] I have reviewed this pull request (self-review) ## Future Work - If/when Bedrock exposes additional Cohere embedding model IDs (e.g. newer versions), we can consider documenting them explicitly, but the provider already supports arbitrary model IDs via `string` model IDs. ## Related Issues Fixes - [#5055](#5055) - [#9390](#9390) --------- Co-authored-by: Aayush Kapoor <83492835+aayush-kapoor@users.noreply.github.com>
1 parent fb5b73e commit 6ece44c

File tree

10 files changed

+239
-11
lines changed

10 files changed

+239
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/amazon-bedrock': patch
3+
---
4+
5+
Fix Cohere embedding model request format on Bedrock by sending the required `input_type` and parsing Cohere-style responses.

content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,13 @@ The following optional provider options are available for Bedrock Titan embeddin
758758
| `cohere.embed-english-v3` | 1024 | <Cross size={18} /> |
759759
| `cohere.embed-multilingual-v3` | 1024 | <Cross size={18} /> |
760760

761+
<Note>
762+
Cohere embedding models on Bedrock require an <code>input_type</code>. Set it
763+
via
764+
<code>providerOptions.bedrock.inputType</code> (defaults to{' '}
765+
<code>search_query</code>).
766+
</Note>
767+
761768
## Reranking Models
762769

763770
You can create models that call the [Bedrock Rerank API](https://docs.aws.amazon.com/bedrock/latest/userguide/rerank-api.html)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { bedrock } from '@ai-sdk/amazon-bedrock';
2+
import { embedMany } from 'ai';
3+
import { run } from '../lib/run';
4+
5+
run(async () => {
6+
const { embeddings, usage, warnings } = await embedMany({
7+
model: bedrock.embedding('cohere.embed-v4:0'),
8+
values: [
9+
'sunny day at the beach',
10+
'rainy afternoon in the city',
11+
'snowy night in the mountains',
12+
],
13+
});
14+
15+
console.log(embeddings);
16+
console.log(usage);
17+
console.log(warnings);
18+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { bedrock } from '@ai-sdk/amazon-bedrock';
2+
import { embedMany } from 'ai';
3+
import { run } from '../lib/run';
4+
5+
run(async () => {
6+
const { embeddings, usage, warnings } = await embedMany({
7+
model: bedrock.embedding('cohere.embed-english-v3'),
8+
values: [
9+
'sunny day at the beach',
10+
'rainy afternoon in the city',
11+
'snowy night in the mountains',
12+
],
13+
});
14+
15+
console.log(embeddings);
16+
console.log(usage);
17+
console.log(warnings);
18+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { bedrock } from '@ai-sdk/amazon-bedrock';
2+
import { embed } from 'ai';
3+
import { run } from '../lib/run';
4+
5+
run(async () => {
6+
// Use 'search_document' for documents to be searched,
7+
// and 'search_query' for search queries (default).
8+
const { embedding, usage, warnings } = await embed({
9+
model: bedrock.embedding('cohere.embed-english-v3'),
10+
value: 'sunny day at the beach',
11+
providerOptions: {
12+
bedrock: {
13+
inputType: 'search_document',
14+
},
15+
},
16+
});
17+
18+
console.log(embedding);
19+
console.log(usage);
20+
console.log(warnings);
21+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { bedrock } from '@ai-sdk/amazon-bedrock';
2+
import { embed } from 'ai';
3+
import { run } from '../lib/run';
4+
5+
run(async () => {
6+
const { embedding, usage, warnings } = await embed({
7+
model: bedrock.embedding('cohere.embed-v4:0'),
8+
value: 'sunny day at the beach',
9+
});
10+
11+
console.log(embedding);
12+
console.log(usage);
13+
console.log(warnings);
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { bedrock } from '@ai-sdk/amazon-bedrock';
2+
import { embed } from 'ai';
3+
import { run } from '../lib/run';
4+
5+
run(async () => {
6+
const { embedding, usage, warnings } = await embed({
7+
model: bedrock.embedding('cohere.embed-english-v3'),
8+
value: 'sunny day at the beach',
9+
});
10+
11+
console.log(embedding);
12+
console.log(usage);
13+
console.log(warnings);
14+
});

packages/amazon-bedrock/src/bedrock-embedding-model.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ const embedUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encode
1616
'amazon.titan-embed-text-v2:0',
1717
)}/invoke`;
1818

19+
const cohereEmbedUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encodeURIComponent(
20+
'cohere.embed-english-v3',
21+
)}/invoke`;
22+
23+
const cohereV4EmbedUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encodeURIComponent(
24+
'cohere.embed-v4:0',
25+
)}/invoke`;
26+
1927
describe('doEmbed', () => {
2028
const mockConfigHeaders = {
2129
'config-header': 'config-value',
@@ -37,6 +45,32 @@ describe('doEmbed', () => {
3745
),
3846
},
3947
},
48+
[cohereEmbedUrl]: {
49+
response: {
50+
type: 'binary',
51+
headers: {
52+
'content-type': 'application/json',
53+
},
54+
body: Buffer.from(
55+
JSON.stringify({
56+
embeddings: [mockEmbeddings[0]],
57+
}),
58+
),
59+
},
60+
},
61+
[cohereV4EmbedUrl]: {
62+
response: {
63+
type: 'binary',
64+
headers: {
65+
'content-type': 'application/json',
66+
},
67+
body: Buffer.from(
68+
JSON.stringify({
69+
embeddings: { float: [mockEmbeddings[0]] },
70+
}),
71+
),
72+
},
73+
},
4074
});
4175

4276
const model = new BedrockEmbeddingModel('amazon.titan-embed-text-v2:0', {
@@ -87,6 +121,52 @@ describe('doEmbed', () => {
87121
expect(usage?.tokens).toStrictEqual(8);
88122
});
89123

124+
it('should support Cohere embedding models', async () => {
125+
const cohereModel = new BedrockEmbeddingModel('cohere.embed-english-v3', {
126+
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
127+
headers: mockConfigHeaders,
128+
fetch: fakeFetchWithAuth,
129+
});
130+
131+
const { embeddings, usage } = await cohereModel.doEmbed({
132+
values: [testValues[0]],
133+
});
134+
135+
expect(embeddings.length).toBe(1);
136+
expect(embeddings[0]).toStrictEqual(mockEmbeddings[0]);
137+
expect(Number.isNaN(usage?.tokens)).toBe(true);
138+
139+
const body = await server.calls[0].requestBodyJson;
140+
expect(body).toEqual({
141+
input_type: 'search_query',
142+
texts: [testValues[0]],
143+
truncate: undefined,
144+
});
145+
});
146+
147+
it('should support Cohere v4 embedding models', async () => {
148+
const cohereV4Model = new BedrockEmbeddingModel('cohere.embed-v4:0', {
149+
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
150+
headers: mockConfigHeaders,
151+
fetch: fakeFetchWithAuth,
152+
});
153+
154+
const { embeddings, usage } = await cohereV4Model.doEmbed({
155+
values: [testValues[0]],
156+
});
157+
158+
expect(embeddings.length).toBe(1);
159+
expect(embeddings[0]).toStrictEqual(mockEmbeddings[0]);
160+
expect(Number.isNaN(usage?.tokens)).toBe(true);
161+
162+
const body = await server.calls[0].requestBodyJson;
163+
expect(body).toEqual({
164+
input_type: 'search_query',
165+
texts: [testValues[0]],
166+
truncate: undefined,
167+
});
168+
});
169+
90170
it('should properly combine headers from all sources', async () => {
91171
const optionsHeaders = {
92172
'options-header': 'options-value',

packages/amazon-bedrock/src/bedrock-embedding-model.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,23 @@ export class BedrockEmbeddingModel implements EmbeddingModelV3 {
6767
})) ?? {};
6868

6969
// 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-
};
70+
//
71+
// Note: Different embedding model families expect different request/response
72+
// payloads (e.g. Titan vs Cohere). We keep the public interface stable and
73+
// adapt here based on the modelId.
74+
const args = this.modelId.startsWith('cohere.embed-')
75+
? {
76+
// Cohere embedding models on Bedrock require `input_type`.
77+
// Without it, the service attempts other schema branches and rejects the request.
78+
input_type: bedrockOptions.inputType ?? 'search_query',
79+
texts: [values[0]],
80+
truncate: bedrockOptions.truncate,
81+
}
82+
: {
83+
inputText: values[0],
84+
dimensions: bedrockOptions.dimensions,
85+
normalize: bedrockOptions.normalize,
86+
};
7587
const url = this.getUrl(this.modelId);
7688
const { value: response } = await postJsonToApi({
7789
url,
@@ -90,15 +102,40 @@ export class BedrockEmbeddingModel implements EmbeddingModelV3 {
90102
abortSignal,
91103
});
92104

105+
const embedding =
106+
'embedding' in response
107+
? response.embedding
108+
: Array.isArray(response.embeddings)
109+
? response.embeddings[0]
110+
: response.embeddings.float[0];
111+
93112
return {
94113
warnings: [],
95-
embeddings: [response.embedding],
96-
usage: { tokens: response.inputTextTokenCount },
114+
embeddings: [embedding],
115+
usage: {
116+
tokens:
117+
'inputTextTokenCount' in response
118+
? response.inputTextTokenCount
119+
: NaN,
120+
},
97121
};
98122
}
99123
}
100124

101-
const BedrockEmbeddingResponseSchema = z.object({
102-
embedding: z.array(z.number()),
103-
inputTextTokenCount: z.number(),
104-
});
125+
const BedrockEmbeddingResponseSchema = z.union([
126+
// Titan-style response
127+
z.object({
128+
embedding: z.array(z.number()),
129+
inputTextTokenCount: z.number(),
130+
}),
131+
// Cohere v3-style response
132+
z.object({
133+
embeddings: z.array(z.array(z.number())),
134+
}),
135+
// Cohere v4-style response
136+
z.object({
137+
embeddings: z.object({
138+
float: z.array(z.array(z.number())),
139+
}),
140+
}),
141+
]);

packages/amazon-bedrock/src/bedrock-embedding-options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,18 @@ Flag indicating whether or not to normalize the output embeddings. Defaults to t
2121
Only supported in amazon.titan-embed-text-v2:0.
2222
*/
2323
normalize: z.boolean().optional(),
24+
25+
/**
26+
Input type for Cohere embedding models on Bedrock.
27+
Common values: `search_document`, `search_query`, `classification`, `clustering`.
28+
If not set, the provider defaults to `search_query`.
29+
*/
30+
inputType: z
31+
.enum(['search_document', 'search_query', 'classification', 'clustering'])
32+
.optional(),
33+
34+
/**
35+
Truncation behavior for Cohere embedding models on Bedrock.
36+
*/
37+
truncate: z.enum(['NONE', 'START', 'END']).optional(),
2438
});

0 commit comments

Comments
 (0)