From 42b5141d8142ab1626834c09bee9703907ad5933 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 25 Jul 2026 12:51:47 +0000 Subject: [PATCH] fix(ai): preserved Bedrock ARN thinking signatures Replay captured Bedrock reasoning signatures independently of model-id family detection so Claude-backed inference-profile ARNs retain signed thinking blocks across turns. Add an adaptive-thinking ARN request regression test and changelog entry. Fixes #6610 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/src/providers/amazon-bedrock.ts | 7 +- .../ai/test/bedrock-inference-profile.test.ts | 72 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 67c35dd17..601736343 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Bedrock Converse dropping captured Claude thinking signatures when replaying application-inference-profile ARN models, restoring adaptive-thinking multi-turn conversations ([#6610](https://github.com/can1357/oh-my-pi/issues/6610)). + ## [17.1.3] - 2026-07-24 ### Fixed diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 036575b05..61800b09b 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -830,10 +830,9 @@ function convertMessages( case "thinking": // Skip empty thinking blocks if (c.thinking.trim().length === 0) continue; - // Thinking blocks require a valid signature when sent as reasoningContent. - // If the signature is missing (e.g., from an aborted stream), or the model - // doesn't support signatures, convert to plain text instead. - if (supportsThinkingSignature(model) && c.thinkingSignature) { + // A captured signature is authoritative even when the model id is an opaque ARN. + // Without one, known non-Claude families use unsigned reasoning; known Claude ids demote to text. + if (c.thinkingSignature) { contentBlocks.push({ reasoningContent: { reasoningText: { text: c.thinking.toWellFormed(), signature: c.thinkingSignature }, diff --git a/packages/ai/test/bedrock-inference-profile.test.ts b/packages/ai/test/bedrock-inference-profile.test.ts index 08314bb84..495ad7a44 100644 --- a/packages/ai/test/bedrock-inference-profile.test.ts +++ b/packages/ai/test/bedrock-inference-profile.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { streamBedrock } from "@oh-my-pi/pi-ai/providers/amazon-bedrock"; import type { Context, FetchImpl, Model } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { Effort } from "@oh-my-pi/pi-catalog/effort"; import { withEnv } from "./helpers"; const profileArn = "arn:aws:bedrock:us-east-2:1234567890:application-inference-profile/company-opus-48"; @@ -16,6 +17,11 @@ const profileModel: Model<"bedrock-converse-stream"> = buildModel({ cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, contextWindow: 1000000, maxTokens: 128000, + thinking: { + mode: "anthropic-adaptive", + efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.Max], + supportsDisplay: true, + }, }); function userContext(): Context { @@ -46,6 +52,72 @@ describe("Bedrock inference profile ARNs", () => { `https://bedrock-runtime.us-east-2.amazonaws.com/model/${encodeURIComponent(profileArn)}/converse-stream`, ]); }); + + test("replays captured thinking signatures for ARN profiles", async () => { + const context: Context = { + messages: [ + { role: "user", content: "Plan the change", timestamp: 0 }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "Inspect the implementation", thinkingSignature: "signed-reasoning" }, + { type: "text", text: "I found the relevant code." }, + ], + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + model: profileArn, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: 1, + }, + { role: "user", content: "Continue", timestamp: 2 }, + ], + }; + const controller = new AbortController(); + controller.abort(); + const { promise, resolve } = Promise.withResolvers(); + + void streamBedrock(profileModel, context, { + signal: controller.signal, + reasoning: Effort.High, + maxTokens: 16, + onPayload: payload => { + resolve(payload); + }, + }); + + expect(await promise).toMatchObject({ + additionalModelRequestFields: { + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "high" }, + }, + messages: [ + { role: "user", content: [{ text: "Plan the change" }] }, + { + role: "assistant", + content: [ + { + reasoningContent: { + reasoningText: { + text: "Inspect the implementation", + signature: "signed-reasoning", + }, + }, + }, + { text: "I found the relevant code." }, + ], + }, + { role: "user", content: [{ text: "Continue" }] }, + ], + }); + }); }); function bedrockModel(id: string): Model<"bedrock-converse-stream"> {