From 65f254223bee4c9db42a694d1855cb7405951f32 Mon Sep 17 00:00:00 2001 From: Adnan Hajar Date: Mon, 20 Apr 2026 18:46:21 -0400 Subject: [PATCH] feat(amazon-bedrock): forward default Anthropic betas via additionalModelRequestFields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bedrock Converse ignores the `anthropic-beta` HTTP header that the direct Anthropic API honors. The Bedrock-correct channel is the top-level `additionalModelRequestFields.anthropic_beta` array on the ConverseStream request, which Bedrock forwards to the underlying Anthropic model. Mirror `PI_AI_DEFAULT_ANTHROPIC_BETAS` from extensions/anthropic/stream-wrappers.ts so Bedrock-hosted Claude behaves the same as the direct Anthropic API: - fine-grained-tool-streaming-2025-05-14 - interleaved-thinking-2025-05-14 Motivation: `fine-grained-tool-streaming` makes large tool_use input JSON (e.g. multi-KB heredoc scripts) stream incrementally via input_json_delta events instead of being buffered until the tool_use block is complete. Users observed 60-80s of silence on turns where the model generates large tool_use arguments on Bedrock — a gap that does not exist on the direct Anthropic path because the anthropic extension already sets these betas. Surgical change: swap the Anthropic branch of `baseWrapStreamFn` from a no-op passthrough to a wrapper that injects the betas. Non-Anthropic families are unchanged. --- extensions/amazon-bedrock/index.test.ts | 22 ++++++++++++++++++- .../amazon-bedrock/register.sync.runtime.ts | 20 ++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index a4b541e17ce28..4653abf42e587 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -264,7 +264,7 @@ describe("amazon-bedrock provider plugin", () => { }); const result = callWrappedStream(provider, ANTHROPIC_MODEL, ANTHROPIC_MODEL_DESCRIPTOR); - // Anthropic models should get guardrailConfig + // Anthropic models get guardrailConfig + default anthropic_beta flags. expect(result._capturedPayload).toEqual({ guardrailConfig: { guardrailIdentifier: "guardrail-anthropic", @@ -272,11 +272,31 @@ describe("amazon-bedrock provider plugin", () => { streamProcessingMode: "async", trace: "disabled", }, + additionalModelRequestFields: { + anthropic_beta: [ + "fine-grained-tool-streaming-2025-05-14", + "interleaved-thinking-2025-05-14", + ], + }, }); // Anthropic models should NOT get cacheRetention: "none" expect(result).not.toHaveProperty("cacheRetention", "none"); }); + it("injects default anthropic_beta flags for Anthropic Bedrock models without guardrail", async () => { + const provider = await registerWithConfig(undefined); + const result = callWrappedStream(provider, ANTHROPIC_MODEL, ANTHROPIC_MODEL_DESCRIPTOR); + + expect(result._capturedPayload).toEqual({ + additionalModelRequestFields: { + anthropic_beta: [ + "fine-grained-tool-streaming-2025-05-14", + "interleaved-thinking-2025-05-14", + ], + }, + }); + }); + it("injects guardrailConfig for non-Anthropic models with cacheRetention: none", async () => { const provider = await registerWithConfig({ guardrail: { diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index a141ffd344478..70035cec80c88 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -16,6 +16,22 @@ import { } from "./api.js"; import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js"; +const PI_AI_DEFAULT_ANTHROPIC_BETAS = [ + "fine-grained-tool-streaming-2025-05-14", + "interleaved-thinking-2025-05-14", +] as const; + +function createBedrockAnthropicBetasWrapper(baseStreamFn: StreamFn | undefined): StreamFn | undefined { + if (!baseStreamFn) return baseStreamFn; + return (model, context, options) => + streamWithPayloadPatch(baseStreamFn, model, context, options, (payload) => { + payload.additionalModelRequestFields = { + ...(payload.additionalModelRequestFields as Record | undefined), + anthropic_beta: [...PI_AI_DEFAULT_ANTHROPIC_BETAS], + }; + }); +} + type GuardrailConfig = { guardrailIdentifier: string; guardrailVersion: string; @@ -82,7 +98,9 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { api.registerMemoryEmbeddingProvider(bedrockMemoryEmbeddingProviderAdapter); const baseWrapStreamFn = ({ modelId, streamFn }: { modelId: string; streamFn?: StreamFn }) => - isAnthropicBedrockModel(modelId) ? streamFn : createBedrockNoCacheWrapper(streamFn); + isAnthropicBedrockModel(modelId) + ? createBedrockAnthropicBetasWrapper(streamFn) + : createBedrockNoCacheWrapper(streamFn); const cacheWrapStreamFn = guardrail?.guardrailIdentifier && guardrail?.guardrailVersion