Merge PR #6613: fix(ai): preserved Bedrock ARN thinking signatures (@roboomp)

This commit is contained in:
can1357
2026-07-26 15:38:52 +02:00
3 changed files with 76 additions and 4 deletions
+1
View File
@@ -20,6 +20,7 @@
- Scoped Codex reactive backoff per meter: a `usage_limit_reached` from a Spark request no longer persists a block that ordinary chat requests honour, and the reverse. Blocks written before scoping used a shared scope meaning "block everything", so requests still honour it and reconciliation still heals it
- Implemented `scopeLimits` for the Codex ranking strategy so a request gates only on the windows it actually consumes: `-spark` models spend the Spark meter and every other model spends the 5h/weekly chat windows, instead of OR-ing every window and meter into one provider-wide block
- Fixed native Anthropic adaptive-only models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5) keeping thinking ON when reasoning was meant to be off. `mapOptionsForApi` never consulted `disableReasoning` on the Anthropic branch, so a caller-side disable left adaptive thinking at full effort; and `disableThinkingIfToolChoiceForced` deleted `output_config.effort` alongside `thinking`, which for adaptive-only models silently re-enabled adaptive thinking (a bare omission defaults to adaptive-ON). Both paths now omit `thinking` and pin the lowest adaptive effort, so `disableReasoning` and forced `tool_choice` turns (e.g. the delivery reviewer's `report_delivery`) actually suppress reasoning instead of returning a thinking block with `end_turn` ([#6589](https://github.com/can1357/oh-my-pi/issues/6589)).
- 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
+3 -4
View File
@@ -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 },
@@ -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<unknown>();
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"> {