From 42f34b65ededf4a1fbf3cd4bbff486ddfeb320e9 Mon Sep 17 00:00:00 2001 From: Alexander Olsen Date: Mon, 22 Jun 2026 11:12:59 -0400 Subject: [PATCH] feat(deepagents): add bedrockPromptCachingMiddleware to default stack (#611) ## Summary - Adds `bedrockPromptCachingMiddleware` to the default middleware stack - Adds unit tests for the Bedrock model detection util function covering the different formats of model strings/`ChatModel`s --- .changeset/lemon-roses-tell.md | 7 +++ libs/deepagents/src/agent.ts | 31 +++++++---- libs/deepagents/src/utils.test.ts | 86 +++++++++++++++++++++++++++++++ libs/deepagents/src/utils.ts | 30 +++++++++++ 4 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 .changeset/lemon-roses-tell.md create mode 100644 libs/deepagents/src/utils.test.ts diff --git a/.changeset/lemon-roses-tell.md b/.changeset/lemon-roses-tell.md new file mode 100644 index 00000000..2ffeed28 --- /dev/null +++ b/.changeset/lemon-roses-tell.md @@ -0,0 +1,7 @@ +--- +"deepagents": patch +--- + +feat(deepagents): add bedrockPromptCachingMiddleware to default stack + +Add bedrockPromptCachingMiddleware to default middleware stack. This automatically opts-in to Bedrock prompt caching for Nova and Anthropic models diff --git a/libs/deepagents/src/agent.ts b/libs/deepagents/src/agent.ts index b6f7a892..42b73b8f 100644 --- a/libs/deepagents/src/agent.ts +++ b/libs/deepagents/src/agent.ts @@ -3,6 +3,7 @@ import { createMiddleware, humanInTheLoopMiddleware, anthropicPromptCachingMiddleware, + bedrockPromptCachingMiddleware, todoListMiddleware, SystemMessage, type AgentMiddleware, @@ -61,6 +62,7 @@ import { isAnthropicModel, getModelProvider, getModelIdentifier, + isBedrockConverseModel, } from "./utils.js"; const BASE_AGENT_PROMPT = context` @@ -220,15 +222,26 @@ export function createDeepAgent< : (tools as StructuredTool[]); const anthropicModel = isAnthropicModel(model); - const cacheMiddleware = anthropicModel - ? [ - anthropicPromptCachingMiddleware({ - unsupportedModelBehavior: "ignore", - minMessagesToCache: 1, - }), - createCacheBreakpointMiddleware(), - ] - : []; + const bedrockModel = isBedrockConverseModel(model); + let cacheMiddleware: AnyAgentMiddleware[] = []; + + if (anthropicModel) { + cacheMiddleware = [ + ...cacheMiddleware, + anthropicPromptCachingMiddleware({ + unsupportedModelBehavior: "ignore", + minMessagesToCache: 1, + }), + createCacheBreakpointMiddleware(), + ]; + } + + if (bedrockModel) { + cacheMiddleware = [ + ...cacheMiddleware, + bedrockPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }), + ]; + } /** * Process subagents to add SkillsMiddleware for those with their own skills. diff --git a/libs/deepagents/src/utils.test.ts b/libs/deepagents/src/utils.test.ts new file mode 100644 index 00000000..92810177 --- /dev/null +++ b/libs/deepagents/src/utils.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi } from "vitest"; +import { FakeListChatModel } from "@langchain/core/utils/testing"; +import { isBedrockConverseModel } from "./utils.js"; + +describe("isBedrockConverseModel", () => { + describe("string inputs", () => { + it("should detect bedrock: prefixed model strings", () => { + expect( + isBedrockConverseModel( + "bedrock:anthropic.claude-3-5-sonnet-20240620-v1:0", + ), + ).toBe(true); + + expect( + isBedrockConverseModel( + "bedrock:us.anthropic.claude-3-7-sonnet-20250219-v1:0", + ), + ).toBe(true); + + expect( + isBedrockConverseModel("bedrock:meta.llama3-70b-instruct-v1:0"), + ).toBe(true); + }); + + it("should detect aws: prefixed model strings", () => { + expect( + isBedrockConverseModel("aws:anthropic.claude-3-5-sonnet-20240620-v1:0"), + ).toBe(true); + expect(isBedrockConverseModel("aws:amazon.nova-pro-v1:0")).toBe(true); + }); + + it("should detect bare amazon.* model strings (langchain auto-inference)", () => { + expect(isBedrockConverseModel("amazon.nova-pro-v1:0")).toBe(true); + expect(isBedrockConverseModel("amazon.titan-text-express-v1")).toBe(true); + }); + + it("should reject non-Bedrock provider-prefixed model strings", () => { + expect(isBedrockConverseModel("anthropic:claude-3-opus")).toBe(false); + expect(isBedrockConverseModel("openai:gpt-4")).toBe(false); + expect(isBedrockConverseModel("google:gemini-pro")).toBe(false); + }); + + it("should reject bare non-amazon model strings", () => { + expect( + isBedrockConverseModel("anthropic.claude-3-5-sonnet-20240620-v1:0"), + ).toBe(false); + expect(isBedrockConverseModel("meta.llama3-70b-instruct-v1:0")).toBe( + false, + ); + }); + }); + + describe("model object inputs", () => { + it("should detect ChatBedrockConverse model objects", () => { + const model = new FakeListChatModel({ responses: [] }); + vi.spyOn(model, "getName").mockReturnValue("ChatBedrockConverse"); + expect(isBedrockConverseModel(model)).toBe(true); + }); + + it("should reject non-Bedrock model objects", () => { + const anthropic = new FakeListChatModel({ responses: [] }); + vi.spyOn(anthropic, "getName").mockReturnValue("ChatAnthropic"); + expect(isBedrockConverseModel(anthropic)).toBe(false); + }); + + it("should detect ConfigurableModel wrapping the bedrock provider", () => { + const model = new FakeListChatModel({ responses: [] }); + vi.spyOn(model, "getName").mockReturnValue("ConfigurableModel"); + (model as any)._defaultConfig = { modelProvider: "bedrock" }; + expect(isBedrockConverseModel(model)).toBe(true); + }); + + it("should detect ConfigurableModel wrapping the aws provider alias", () => { + const model = new FakeListChatModel({ responses: [] }); + vi.spyOn(model, "getName").mockReturnValue("ConfigurableModel"); + (model as any)._defaultConfig = { modelProvider: "aws" }; + expect(isBedrockConverseModel(model)).toBe(true); + }); + + it("should reject ConfigurableModel with no _defaultConfig", () => { + const model = new FakeListChatModel({ responses: [] }); + vi.spyOn(model, "getName").mockReturnValue("ConfigurableModel"); + expect(isBedrockConverseModel(model)).toBe(false); + }); + }); +}); diff --git a/libs/deepagents/src/utils.ts b/libs/deepagents/src/utils.ts index d80b3ab4..aa48513a 100644 --- a/libs/deepagents/src/utils.ts +++ b/libs/deepagents/src/utils.ts @@ -26,6 +26,36 @@ export function isAnthropicModel( return model.getName() === "ChatAnthropic"; } +/** + * Detect whether a model is an AWS Bedrock Converse model. + * + * Accepts the wider `RunnableInterface` shape (the type of `request.model` + * inside `wrapModelCall`, aliased as `AgentLanguageModelLike` in langchain) + * because the function only depends on `.getName()`, which is part of the + * Runnable contract. `BaseLanguageModel` extends `Runnable`, so existing + * call sites still type-check. + */ +export function isBedrockConverseModel( + model: BaseLanguageModel | RunnableInterface | string, +): boolean { + if (typeof model === "string") { + // Explicit provider prefix (`bedrock:` or `aws:`) — both map to + // ChatBedrockConverse in langchain's initChatModel. + const colonIdx = model.indexOf(":"); + if (colonIdx !== -1) { + const prefix = model.slice(0, colonIdx); + if (prefix === "bedrock" || prefix === "aws") return true; + } + + return model.startsWith("amazon."); + } + if (model.getName() === "ConfigurableModel") { + const provider = (model as any)._defaultConfig?.modelProvider; + return provider === "bedrock" || provider === "aws"; + } + return model.getName() === "ChatBedrockConverse"; +} + /** * Extract the provider name from a model instance for profile lookup. *