Skip to content

Commit 3cdc4f6

Browse files
PierrunoYTclaude
andcommitted
fix(bedrock): stop reasoning replay at a new user message
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
1 parent 4088b89 commit 3cdc4f6

3 files changed

Lines changed: 34 additions & 9 deletions

File tree

‎src/api/providers/__tests__/bedrock.spec.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,17 @@ describe("AwsBedrockHandler", () => {
184184
content: [
185185
{ type: "thinking", thinking: "Signed by MiniMax.", signature: "minimax-signature" },
186186
{ type: "text", text: "Done." },
187+
{ type: "tool_use", id: "t1", name: "read_file", input: {} },
187188
],
188189
},
189-
{ role: "user", content: "next" },
190+
{ role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }] },
190191
] as Anthropic.Messages.MessageParam[]
191192
await collectStream(provider.createMessage("system", history))
192193
const [{ messages }] = mockConverseStreamCommand.mock.lastCall!
193194
expect(messages?.[1].content).toStrictEqual([
194195
...(kept ? [{ reasoningContent: { reasoningText: { text: "Signed by MiniMax." } } }] : []),
195196
{ text: "Done." },
197+
{ toolUse: { toolUseId: "t1", name: "read_file", input: {} } },
196198
])
197199
})
198200

‎src/api/transform/__tests__/bedrock-converse-format.spec.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ describe("convertToBedrockConverseMessages", () => {
5050
])
5151
})
5252

53+
it("replays reasoning into a tool_result continuation but drops it before a new user message", () => {
54+
const reasoning = { type: "reasoning", text: "Plan.", summary: [] }
55+
const messages = [
56+
{ role: "assistant", content: [reasoning, { type: "tool_use", id: "t1", name: "read", input: {} }] },
57+
{ role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }] },
58+
{ role: "assistant", content: [reasoning, { type: "text", text: "Done." }] },
59+
{ role: "user", content: [{ type: "text", text: "next" }] },
60+
] as unknown as Anthropic.Messages.MessageParam[]
61+
62+
const result = convertToBedrockConverseMessages(messages, { preserveReasoning: true })
63+
64+
expect(result[0].content?.[0]).toEqual({ reasoningContent: { reasoningText: { text: "Plan." } } })
65+
expect(result[2].content).toEqual([{ text: "Done." }])
66+
})
67+
5368
it("drops reasoning and thinking blocks for models that do not preserve reasoning", () => {
5469
// Signed thinking bypasses Task's reasoning filter, so the converter must not send it to e.g. Claude.
5570
const messages = [

‎src/api/transform/bedrock-converse-format.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ interface BedrockMessageContent {
2727
/**
2828
* Convert Anthropic messages to Bedrock Converse format
2929
* @param anthropicMessages Messages in Anthropic format
30-
* @param options.preserveReasoning Replay reasoning/thinking blocks as `reasoningContent`.
30+
* @param options.preserveReasoning Replay reasoning/thinking blocks as `reasoningContent` while a tool loop
31+
* continues; reasoning followed by a new user message is dropped.
3132
* Otherwise they are dropped, matching Task's stripping for models without `preserveReasoning`.
3233
*/
3334
export function convertToBedrockConverseMessages(
3435
anthropicMessages: Anthropic.Messages.MessageParam[],
3536
{ preserveReasoning = false }: { preserveReasoning?: boolean } = {},
3637
): Message[] {
37-
return anthropicMessages.map((anthropicMessage) => {
38+
return anthropicMessages.map((anthropicMessage, index) => {
3839
// Map Anthropic roles to Bedrock roles
3940
const role: ConversationRole = anthropicMessage.role === "assistant" ? "assistant" : "user"
4041

@@ -52,12 +53,19 @@ export function convertToBedrockConverseMessages(
5253
// Signed thinking blocks (e.g. from MiniMax) bypass Task's reasoning filter, so they can reach
5354
// any Bedrock model after a provider switch. Unsigned reasoning is only safe for models that
5455
// opt in; Claude would receive thinking it cannot verify.
55-
const blocks = preserveReasoning
56-
? anthropicMessage.content
57-
: anthropicMessage.content.filter((block) => {
58-
const type = (block as { type: string }).type
59-
return type !== "reasoning" && type !== "thinking"
60-
})
56+
// Reasoning continuity only spans a tool loop: a following new user message ends it.
57+
const next = anthropicMessages[index + 1]
58+
const endsAtNewUserMessage =
59+
next?.role === "user" &&
60+
(typeof next.content === "string" ||
61+
!next.content.some((block) => (block as { type: string }).type === "tool_result"))
62+
const blocks =
63+
preserveReasoning && !endsAtNewUserMessage
64+
? anthropicMessage.content
65+
: anthropicMessage.content.filter((block) => {
66+
const type = (block as { type: string }).type
67+
return type !== "reasoning" && type !== "thinking"
68+
})
6169

6270
// Process complex content types
6371
const content = blocks.map((block) => {

0 commit comments

Comments
 (0)