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
This commit is contained in:
Alexander Olsen
2026-06-22 11:12:59 -04:00
committed by GitHub
parent 3c8f8b2ea6
commit 42f34b65ed
4 changed files with 145 additions and 9 deletions
+7
View File
@@ -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
+22 -9
View File
@@ -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.
+86
View File
@@ -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);
});
});
});
+30
View File
@@ -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<unknown, unknown> | 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.
*