Skip to content

Commit 4918dda

Browse files
Backport: fix(amazon-bedrock): preserve reasoning text when signature is present (#13979)
This is an automated backport of #13972 to the release-v6.0 branch. FYI @dancer Co-authored-by: dancer <144584931+dancer@users.noreply.github.com>
1 parent 1d18256 commit 4918dda

File tree

3 files changed

+293
-45
lines changed

3 files changed

+293
-45
lines changed

.changeset/fast-dots-draw.md

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(amazon-bedrock): preserve reasoning text when signature is present

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

Lines changed: 255 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ describe('assistant messages', () => {
669669
});
670670
});
671671

672-
it('should trim trailing whitespace from reasoning content when it is the last part', async () => {
672+
it('should not trim reasoning text when a signature is present', async () => {
673673
const result = await convertToBedrockChatMessages([
674674
{
675675
role: 'user',
@@ -691,28 +691,263 @@ describe('assistant messages', () => {
691691
},
692692
]);
693693

694-
expect(result).toEqual({
695-
messages: [
696-
{
697-
role: 'user',
698-
content: [{ text: 'Explain your reasoning' }],
699-
},
700-
{
701-
role: 'assistant',
702-
content: [
703-
{
704-
reasoningContent: {
705-
reasoningText: {
706-
text: 'This is my reasoning with trailing space',
707-
signature: 'test-signature',
694+
expect(result).toMatchInlineSnapshot(`
695+
{
696+
"messages": [
697+
{
698+
"content": [
699+
{
700+
"text": "Explain your reasoning",
701+
},
702+
],
703+
"role": "user",
704+
},
705+
{
706+
"content": [
707+
{
708+
"reasoningContent": {
709+
"reasoningText": {
710+
"signature": "test-signature",
711+
"text": "This is my reasoning with trailing space ",
712+
},
713+
},
714+
},
715+
],
716+
"role": "assistant",
717+
},
718+
],
719+
"system": [],
720+
}
721+
`);
722+
});
723+
724+
it('should trim trailing whitespace from reasoning content without signature when it is the last part', async () => {
725+
const result = await convertToBedrockChatMessages([
726+
{
727+
role: 'user',
728+
content: [{ type: 'text', text: 'Explain your reasoning' }],
729+
},
730+
{
731+
role: 'assistant',
732+
content: [
733+
{
734+
type: 'reasoning',
735+
text: 'This is my reasoning with trailing space ',
736+
},
737+
],
738+
},
739+
]);
740+
741+
expect(result).toMatchInlineSnapshot(`
742+
{
743+
"messages": [
744+
{
745+
"content": [
746+
{
747+
"text": "Explain your reasoning",
748+
},
749+
],
750+
"role": "user",
751+
},
752+
{
753+
"content": [
754+
{
755+
"reasoningContent": {
756+
"reasoningText": {
757+
"text": "This is my reasoning with trailing space",
758+
},
759+
},
760+
},
761+
],
762+
"role": "assistant",
763+
},
764+
],
765+
"system": [],
766+
}
767+
`);
768+
});
769+
770+
it('should only trim last reasoning part when multiple reasoning parts have trailing spaces', async () => {
771+
const result = await convertToBedrockChatMessages([
772+
{
773+
role: 'user',
774+
content: [{ type: 'text', text: 'Explain your reasoning' }],
775+
},
776+
{
777+
role: 'assistant',
778+
content: [
779+
{
780+
type: 'reasoning',
781+
text: 'First reasoning with trailing space ',
782+
},
783+
{
784+
type: 'reasoning',
785+
text: 'Second reasoning with trailing space ',
786+
},
787+
],
788+
},
789+
]);
790+
791+
expect(result).toMatchInlineSnapshot(`
792+
{
793+
"messages": [
794+
{
795+
"content": [
796+
{
797+
"text": "Explain your reasoning",
798+
},
799+
],
800+
"role": "user",
801+
},
802+
{
803+
"content": [
804+
{
805+
"reasoningContent": {
806+
"reasoningText": {
807+
"text": "First reasoning with trailing space ",
808+
},
809+
},
810+
},
811+
{
812+
"reasoningContent": {
813+
"reasoningText": {
814+
"text": "Second reasoning with trailing space",
815+
},
708816
},
709817
},
818+
],
819+
"role": "assistant",
820+
},
821+
],
822+
"system": [],
823+
}
824+
`);
825+
});
826+
827+
it('should preserve reasoning text with signature in multi-turn tool use', async () => {
828+
const result = await convertToBedrockChatMessages([
829+
{
830+
role: 'user',
831+
content: [{ type: 'text', text: 'What is the weather?' }],
832+
},
833+
{
834+
role: 'assistant',
835+
content: [
836+
{
837+
type: 'reasoning',
838+
text: 'Let me check the weather API.\n',
839+
providerOptions: {
840+
bedrock: {
841+
signature: 'sig-abc123',
842+
} satisfies BedrockReasoningMetadata,
710843
},
711-
],
712-
},
713-
],
714-
system: [],
715-
});
844+
},
845+
{
846+
type: 'tool-call',
847+
toolCallId: 'call-1',
848+
toolName: 'getWeather',
849+
input: { city: 'SF' },
850+
},
851+
],
852+
},
853+
{
854+
role: 'tool',
855+
content: [
856+
{
857+
type: 'tool-result',
858+
toolCallId: 'call-1',
859+
toolName: 'getWeather',
860+
output: { type: 'text', value: 'Sunny, 72F' },
861+
},
862+
],
863+
},
864+
{
865+
role: 'assistant',
866+
content: [
867+
{
868+
type: 'reasoning',
869+
text: 'The weather is sunny and warm.\n',
870+
providerOptions: {
871+
bedrock: {
872+
signature: 'sig-def456',
873+
} satisfies BedrockReasoningMetadata,
874+
},
875+
},
876+
{ type: 'text', text: 'It is sunny and 72F in SF.' },
877+
],
878+
},
879+
]);
880+
881+
expect(result).toMatchInlineSnapshot(`
882+
{
883+
"messages": [
884+
{
885+
"content": [
886+
{
887+
"text": "What is the weather?",
888+
},
889+
],
890+
"role": "user",
891+
},
892+
{
893+
"content": [
894+
{
895+
"reasoningContent": {
896+
"reasoningText": {
897+
"signature": "sig-abc123",
898+
"text": "Let me check the weather API.
899+
",
900+
},
901+
},
902+
},
903+
{
904+
"toolUse": {
905+
"input": {
906+
"city": "SF",
907+
},
908+
"name": "getWeather",
909+
"toolUseId": "call-1",
910+
},
911+
},
912+
],
913+
"role": "assistant",
914+
},
915+
{
916+
"content": [
917+
{
918+
"toolResult": {
919+
"content": [
920+
{
921+
"text": "Sunny, 72F",
922+
},
923+
],
924+
"toolUseId": "call-1",
925+
},
926+
},
927+
],
928+
"role": "user",
929+
},
930+
{
931+
"content": [
932+
{
933+
"reasoningContent": {
934+
"reasoningText": {
935+
"signature": "sig-def456",
936+
"text": "The weather is sunny and warm.
937+
",
938+
},
939+
},
940+
},
941+
{
942+
"text": "It is sunny and 72F in SF.",
943+
},
944+
],
945+
"role": "assistant",
946+
},
947+
],
948+
"system": [],
949+
}
950+
`);
716951
});
717952

718953
it('should handle a mix of text and reasoning content types', async () => {

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

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -287,33 +287,41 @@ export async function convertToBedrockChatMessages(
287287
schema: bedrockReasoningMetadataSchema,
288288
});
289289

290-
if (reasoningMetadata != null) {
291-
if (reasoningMetadata.signature != null) {
292-
bedrockContent.push({
293-
reasoningContent: {
294-
reasoningText: {
295-
// trim the last text part if it's the last message in the block
296-
// because Bedrock does not allow trailing whitespace
297-
// in pre-filled assistant responses
298-
text: trimIfLast(
299-
isLastBlock,
300-
isLastMessage,
301-
isLastContentPart,
302-
part.text,
303-
),
304-
signature: reasoningMetadata.signature,
305-
},
290+
if (reasoningMetadata?.signature != null) {
291+
// do not trim reasoning text when a signature is present:
292+
// the signature validates the exact original bytes
293+
bedrockContent.push({
294+
reasoningContent: {
295+
reasoningText: {
296+
text: part.text,
297+
signature: reasoningMetadata.signature,
306298
},
307-
});
308-
} else if (reasoningMetadata.redactedData != null) {
309-
bedrockContent.push({
310-
reasoningContent: {
311-
redactedReasoning: {
312-
data: reasoningMetadata.redactedData,
313-
},
299+
},
300+
});
301+
} else if (reasoningMetadata?.redactedData != null) {
302+
bedrockContent.push({
303+
reasoningContent: {
304+
redactedReasoning: {
305+
data: reasoningMetadata.redactedData,
314306
},
315-
});
316-
}
307+
},
308+
});
309+
} else {
310+
// trim the last text part if it's the last message in the block
311+
// because Bedrock does not allow trailing whitespace
312+
// in pre-filled assistant responses
313+
bedrockContent.push({
314+
reasoningContent: {
315+
reasoningText: {
316+
text: trimIfLast(
317+
isLastBlock,
318+
isLastMessage,
319+
isLastContentPart,
320+
part.text,
321+
),
322+
},
323+
},
324+
});
317325
}
318326

319327
break;

0 commit comments

Comments
 (0)