mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): drop stale thinking blocks on Bedrock Claude replays
Send thinking.block_binding with prefix_mismatch_behavior=drop_block and the thinking-binding-controls-2026-08-01 beta for Bedrock Claude Opus 4.7+, Sonnet 5+, and Fable 5, matching the Anthropic provider. Opus 4.6 and Sonnet 4.6 reject the field, and GovCloud is skipped. closes #10324
This commit is contained in:
parent
9fba660cf1
commit
69f0be6f0a
3 changed files with 80 additions and 16 deletions
|
|
@ -17,6 +17,7 @@
|
|||
- Fixed Cloudflare AI Gateway Claude models failing with a 404 by using dashed model IDs (`claude-opus-5-5` instead of `claude-opus-5.5`), which Anthropic requires
|
||||
- Fixed Sign in with ChatGPT continuing when its callback port is taken by another login, which made the browser show "OAuth state mismatch"; it now fails with a port-in-use error ([#10265](https://github.com/earendil-works/pi/issues/10265))
|
||||
- Fixed Amazon Bedrock OpenAI models costing requests above 272k input tokens at the short-context rate; Bedrock models now include the pricing tiers listed on models.dev ([#10326](https://github.com/earendil-works/pi/issues/10326))
|
||||
- Fixed Amazon Bedrock Claude requests failing with "Invalid `signature` in `thinking` block" after the system prompt or tools changed; Claude Opus 4.7+, Sonnet 5+, and Fable 5 now drop stale thinking blocks like the Anthropic provider ([#10324](https://github.com/earendil-works/pi/issues/10324))
|
||||
|
||||
## [1.0.0] - 2026-10-01
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ type Block = (TextContent | ThinkingContent | ToolCall) & {
|
|||
|
||||
const EMPTY_TEXT_PLACEHOLDER = "<empty>";
|
||||
|
||||
const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
|
||||
|
||||
/** Matches the placeholder the Anthropic API path uses for redacted thinking. */
|
||||
const REDACTED_THINKING_PLACEHOLDER = "[Reasoning redacted]";
|
||||
|
||||
|
|
@ -787,6 +789,22 @@ function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boo
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the model accepts `thinking.block_binding`. Opus 4.6 and Sonnet 4.6 reject it with
|
||||
* "thinking.adaptive.block_binding: Extra inputs are not permitted".
|
||||
*/
|
||||
function supportsThinkingBlockBinding(model: Model<"bedrock-converse-stream">): boolean {
|
||||
const candidates = getModelMatchCandidates(model.id, model.name);
|
||||
return candidates.some(
|
||||
(s) =>
|
||||
s.includes("opus-4-7") ||
|
||||
s.includes("opus-4-8") ||
|
||||
s.includes("opus-5") ||
|
||||
s.includes("sonnet-5") ||
|
||||
s.includes("fable-5"),
|
||||
);
|
||||
}
|
||||
|
||||
function mapThinkingLevelToEffort(
|
||||
model: Model<"bedrock-converse-stream">,
|
||||
level: SimpleStreamOptions["reasoning"],
|
||||
|
|
@ -1244,11 +1262,21 @@ function buildAdditionalModelRequestFields(
|
|||
if (isAnthropicClaudeModel(model)) {
|
||||
// GovCloud Bedrock currently rejects the Claude thinking.display field.
|
||||
// Omit it there until the GovCloud Converse schema catches up.
|
||||
const display = isGovCloudBedrockTarget(model, options) ? undefined : (options.thinkingDisplay ?? "summarized");
|
||||
const isGovCloud = isGovCloudBedrockTarget(model, options);
|
||||
const display = isGovCloud ? undefined : (options.thinkingDisplay ?? "summarized");
|
||||
// Replayed signed thinking blocks are bound to the system prompt and tools they were
|
||||
// created with. Bedrock 400s on replay after either changes unless stale blocks are
|
||||
// dropped, matching the Anthropic provider. Skipped on GovCloud like display.
|
||||
const useBlockBinding = !isGovCloud && supportsThinkingBlockBinding(model);
|
||||
const result: Record<string, any> = supportsAdaptiveThinking(model.id, model.name)
|
||||
? {
|
||||
thinking: { type: "adaptive", ...(display !== undefined ? { display } : {}) },
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
...(display !== undefined ? { display } : {}),
|
||||
...(useBlockBinding ? { block_binding: { prefix_mismatch_behavior: "drop_block" } } : {}),
|
||||
},
|
||||
output_config: { effort: mapThinkingLevelToEffort(model, options.reasoning) },
|
||||
...(useBlockBinding ? { anthropic_beta: [THINKING_BINDING_CONTROLS_BETA] } : {}),
|
||||
}
|
||||
: (() => {
|
||||
const defaultBudgets: Record<ThinkingLevel, number> = {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,24 @@ import { hasBedrockCredentials } from "./bedrock-utils.ts";
|
|||
|
||||
interface BedrockThinkingPayload {
|
||||
additionalModelRequestFields?: {
|
||||
thinking?: { type: string; budget_tokens?: number; display?: string };
|
||||
thinking?: {
|
||||
type: string;
|
||||
budget_tokens?: number;
|
||||
display?: string;
|
||||
block_binding?: { prefix_mismatch_behavior: string };
|
||||
};
|
||||
output_config?: { effort?: string };
|
||||
anthropic_beta?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
|
||||
const ADAPTIVE_WITH_BINDING = {
|
||||
type: "adaptive",
|
||||
display: "summarized",
|
||||
block_binding: { prefix_mismatch_behavior: "drop_block" },
|
||||
};
|
||||
|
||||
class PayloadCaptured extends Error {
|
||||
constructor() {
|
||||
super("payload captured");
|
||||
|
|
@ -63,9 +75,9 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model);
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
it("maps xhigh reasoning to effort=xhigh for Claude Opus 4.8", async () => {
|
||||
|
|
@ -78,9 +90,9 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model, { reasoning: "xhigh" });
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
it("uses adaptive thinking for Claude Fable 5 when reasoning is enabled", async () => {
|
||||
|
|
@ -88,9 +100,9 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model);
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
it("uses adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => {
|
||||
|
|
@ -98,9 +110,9 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model);
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
it("uses adaptive thinking for Claude Opus 5 when reasoning is enabled", async () => {
|
||||
|
|
@ -108,9 +120,9 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model);
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
it("maps xhigh reasoning to effort=xhigh for Claude Opus 5", async () => {
|
||||
|
|
@ -118,9 +130,9 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model, { reasoning: "xhigh" });
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
it("maps xhigh reasoning to effort=xhigh for Claude Fable 5", async () => {
|
||||
|
|
@ -128,10 +140,33 @@ describe("Bedrock thinking payload", () => {
|
|||
|
||||
const payload = await capturePayload(model, { reasoning: "xhigh" });
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" });
|
||||
});
|
||||
|
||||
// https://github.com/earendil-works/pi/issues/10324
|
||||
it("sends block_binding and the binding beta for Claude Opus 5.5", async () => {
|
||||
const model = getModel("amazon-bedrock", "global.anthropic.claude-opus-5-5");
|
||||
|
||||
const payload = await capturePayload(model);
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual(ADAPTIVE_WITH_BINDING);
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toEqual([THINKING_BINDING_CONTROLS_BETA]);
|
||||
});
|
||||
|
||||
// Bedrock rejects block_binding on 4.6 models: "Extra inputs are not permitted" (#10324)
|
||||
it.each(["global.anthropic.claude-opus-4-6-v1", "global.anthropic.claude-sonnet-4-6"] as const)(
|
||||
"omits block_binding for %s",
|
||||
async (modelId) => {
|
||||
const model = getModel("amazon-bedrock", modelId);
|
||||
|
||||
const payload = await capturePayload(model);
|
||||
|
||||
expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("omits display for GovCloud model ids on non-adaptive Claude thinking", async () => {
|
||||
const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0");
|
||||
const model: Model<"bedrock-converse-stream"> = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue