Skip to content

Commit 08336f1

Browse files
fix(bedrock): strip file extensions from filename (#12971)
## Background This PR normalizes Bedrock document names derived from part.filename by stripping file extensions before sending requests in order to avoid Bedrock throwing an exception. ## Summary - Added a shared stripFileExtension(filename: string) helper to @ai-sdk/provider-utils. - Exported the helper from provider-utils public index. - Updated Amazon Bedrock chat message conversion to use the helper for document name when part.filename is present. - Updated/added tests for Bedrock conversion behavior. - Added unit tests for the new helper. ## Manual Verfication <details> <summary> repro example:</summary> ```ts import { bedrock } from '@ai-sdk/amazon-bedrock'; import { generateText } from 'ai'; import fs from 'fs'; import { run } from '../../lib/run'; run(async () => { const result = await generateText({ model: bedrock('global.anthropic.claude-sonnet-4-5-20250929-v1:0'), messages: [ { role: 'user', content: [ { type: 'text', text: 'Summarize the content of this text file in a few sentences.', }, { type: 'file', data: fs.readFileSync('./data/error-message.txt'), mediaType: 'text/plain', filename: 'error-message.txt', }, ], }, ], }); console.log('Response:', result.text); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); }); ``` </details> ## Checklist - [x] Tests have been added / updated (for bug fixes / features) - [ ] 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 n/a ## Related Issues Fixes #11518 --------- Co-authored-by: Aayush Kapoor <aayushkapoor34@gmail.com> Co-authored-by: Aayush Kapoor <83492835+aayush-kapoor@users.noreply.github.com>
1 parent dd247d4 commit 08336f1

File tree

6 files changed

+87
-4
lines changed

6 files changed

+87
-4
lines changed

.changeset/twelve-chairs-work.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/amazon-bedrock': patch
3+
'@ai-sdk/provider-utils': patch
4+
---
5+
6+
fix(bedrock): strip file extensions from filename

packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ describe('user messages', () => {
151151
`);
152152
});
153153

154-
it('should be converted with actual filename when provided', async () => {
154+
it('should strip file extension when filename is provided', async () => {
155155
const fileData = new Uint8Array([0, 1, 2, 3]);
156156

157157
const { messages } = await convertToBedrockChatMessages([
@@ -163,7 +163,7 @@ describe('user messages', () => {
163163
type: 'file',
164164
data: Buffer.from(fileData).toString('base64'),
165165
mediaType: 'application/pdf',
166-
filename: 'custom-filename',
166+
filename: 'custom-filename.pdf',
167167
},
168168
],
169169
},
@@ -192,6 +192,43 @@ describe('user messages', () => {
192192
`);
193193
});
194194

195+
it('should preserve filename without extension when provided', async () => {
196+
const fileData = new Uint8Array([0, 1, 2, 3]);
197+
198+
const { messages } = await convertToBedrockChatMessages([
199+
{
200+
role: 'user',
201+
content: [
202+
{
203+
type: 'file',
204+
data: Buffer.from(fileData).toString('base64'),
205+
mediaType: 'application/pdf',
206+
filename: 'custom-filename',
207+
},
208+
],
209+
},
210+
]);
211+
212+
expect(messages).toMatchInlineSnapshot(`
213+
[
214+
{
215+
"content": [
216+
{
217+
"document": {
218+
"format": "pdf",
219+
"name": "custom-filename",
220+
"source": {
221+
"bytes": "AAECAw==",
222+
},
223+
},
224+
},
225+
],
226+
"role": "user",
227+
},
228+
]
229+
`);
230+
});
231+
195232
it('should use consistent document names for prompt cache effectiveness', async () => {
196233
const fileData1 = new Uint8Array([0, 1, 2, 3]);
197234
const fileData2 = new Uint8Array([4, 5, 6, 7]);

packages/amazon-bedrock/src/convert-to-bedrock-chat-messages.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
SharedV3ProviderMetadata,
66
UnsupportedFunctionalityError,
77
} from '@ai-sdk/provider';
8-
import { convertToBase64, parseProviderOptions } from '@ai-sdk/provider-utils';
8+
import {
9+
convertToBase64,
10+
parseProviderOptions,
11+
stripFileExtension,
12+
} from '@ai-sdk/provider-utils';
913
import {
1014
BEDROCK_DOCUMENT_MIME_TYPES,
1115
BEDROCK_IMAGE_MIME_TYPES,
@@ -138,7 +142,9 @@ export async function convertToBedrockChatMessages(
138142
bedrockContent.push({
139143
document: {
140144
format: getBedrockDocumentFormat(part.mediaType),
141-
name: part.filename ?? generateDocumentName(),
145+
name: part.filename
146+
? stripFileExtension(part.filename)
147+
: generateDocumentName(),
142148
source: { bytes: convertToBase64(part.data) },
143149
...(enableCitations && {
144150
citations: { enabled: true },

packages/provider-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export {
5454
type Schema,
5555
type ValidationResult,
5656
} from './schema';
57+
export { stripFileExtension } from './strip-file-extension';
5758
export * from './uint8-utils';
5859
export * from './validate-types';
5960
export { VERSION } from './version';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { stripFileExtension } from './strip-file-extension';
3+
4+
describe('stripFileExtension', () => {
5+
it('should strip the extension from a filename', () => {
6+
expect(stripFileExtension('report.pdf')).toBe('report');
7+
});
8+
9+
it('should return the input when there is no extension', () => {
10+
expect(stripFileExtension('report')).toBe('report');
11+
});
12+
13+
it('should strip all extension segments for multi-dot filenames', () => {
14+
expect(stripFileExtension('archive.tar.gz')).toBe('archive');
15+
});
16+
17+
it('should strip a trailing dot', () => {
18+
expect(stripFileExtension('report.')).toBe('report');
19+
});
20+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Strips file extension segments from a filename.
3+
*
4+
* Examples:
5+
* - "report.pdf" -> "report"
6+
* - "archive.tar.gz" -> "archive"
7+
* - "filename" -> "filename"
8+
*/
9+
export function stripFileExtension(filename: string): string {
10+
const firstDotIndex = filename.indexOf('.');
11+
12+
return firstDotIndex === -1 ? filename : filename.slice(0, firstDotIndex);
13+
}

0 commit comments

Comments
 (0)