Skip to content

Commit 6426c89

Browse files
committed
feat: persist prompts and cache TTL
Restore dropped chat input when a target task disappears, autosave prompt history drafts, and send Bedrock cache point TTLs for eligible Claude models.
1 parent 37b1f8d commit 6426c89

19 files changed

Lines changed: 996 additions & 56 deletions

File tree

packages/types/src/__tests__/bedrock.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ describe("Bedrock model catalog", () => {
5959
expect((bedrockModels["anthropic.claude-sonnet-4-5-20250929-v1:0"] as ModelInfo).maxTokens).toBe(64_000)
6060
expect((bedrockModels["anthropic.claude-haiku-4-5-20251001-v1:0"] as ModelInfo).maxTokens).toBe(64_000)
6161
})
62+
63+
it("marks AWS-documented Claude 4.5 models for 1-hour Bedrock prompt cache TTL", () => {
64+
expect((bedrockModels["anthropic.claude-sonnet-4-5-20250929-v1:0"] as ModelInfo).promptCacheTtl).toBe("1h")
65+
expect((bedrockModels["anthropic.claude-haiku-4-5-20251001-v1:0"] as ModelInfo).promptCacheTtl).toBe("1h")
66+
expect((bedrockModels["anthropic.claude-opus-4-5-20251101-v1:0"] as ModelInfo).promptCacheTtl).toBe("1h")
67+
})
6268
})
6369

6470
describe("resolveBedrockModelInfo", () => {

packages/types/src/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export const modelInfoSchema = z.object({
9898
outputPrice: z.number().optional(),
9999
cacheWritesPrice: z.number().optional(),
100100
cacheReadsPrice: z.number().optional(),
101+
// Bedrock Converse cachePoint TTL values. Omitted means provider default.
102+
promptCacheTtl: z.enum(["5m", "1h"]).optional(),
101103
longContextPricing: z
102104
.object({
103105
thresholdTokens: z.number(),

packages/types/src/providers/bedrock.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const bedrockModels = {
2727
minTokensPerCachePoint: 1024,
2828
maxCachePoints: 4,
2929
cachableFields: ["system", "messages", "tools"],
30+
promptCacheTtl: "1h",
3031
},
3132
"anthropic.claude-sonnet-4-6": {
3233
// Mirrors anthropic-direct cap; AWS Bedrock accepts the same upstream maximum.
@@ -220,6 +221,7 @@ export const bedrockModels = {
220221
minTokensPerCachePoint: 1024,
221222
maxCachePoints: 4,
222223
cachableFields: ["system", "messages", "tools"],
224+
promptCacheTtl: "1h",
223225
},
224226
"anthropic.claude-opus-4-20250514-v1:0": {
225227
maxTokens: 8192,
@@ -289,6 +291,7 @@ export const bedrockModels = {
289291
minTokensPerCachePoint: 2048,
290292
maxCachePoints: 4,
291293
cachableFields: ["system", "messages", "tools"],
294+
promptCacheTtl: "1h",
292295
},
293296
"anthropic.claude-3-5-sonnet-20240620-v1:0": {
294297
maxTokens: 8192,

src/api/providers/__tests__/bedrock.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,8 @@ describe("AwsBedrockHandler", () => {
12521252
// System prompt must exceed minTokensPerCachePoint (1024) for cache points to be placed
12531253
const longSystemPrompt = "You are a helpful assistant. ".repeat(200)
12541254
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
1255+
const findCachePointBlock = (blocks: any[] | undefined) =>
1256+
blocks?.find((block: any) => block.cachePoint !== undefined)
12551257

12561258
it("should enable prompt caching by default when awsUsePromptCache is undefined", async () => {
12571259
const defaultHandler = new AwsBedrockHandler({
@@ -1274,6 +1276,44 @@ describe("AwsBedrockHandler", () => {
12741276
expect(hasCachePoint).toBe(true)
12751277
})
12761278

1279+
it("should set 1h ttl on system cache points for Claude 4.5 models", async () => {
1280+
const claude45Handler = new AwsBedrockHandler({
1281+
apiModelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
1282+
awsAccessKey: "test-access-key",
1283+
awsSecretKey: "test-secret-key",
1284+
awsRegion: "us-east-1",
1285+
})
1286+
1287+
const generator = claude45Handler.createMessage(longSystemPrompt, messages)
1288+
await generator.next()
1289+
1290+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1291+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1292+
1293+
expect(findCachePointBlock(commandArg.system)?.cachePoint).toEqual({ type: "default", ttl: "1h" })
1294+
})
1295+
1296+
it("should set 1h ttl on message cache points for Claude 4.5 models", async () => {
1297+
const claude45Handler = new AwsBedrockHandler({
1298+
apiModelId: "anthropic.claude-sonnet-4-5-20250929-v1:0",
1299+
awsAccessKey: "test-access-key",
1300+
awsSecretKey: "test-secret-key",
1301+
awsRegion: "us-east-1",
1302+
})
1303+
const longUserPrompt = Array.from({ length: 1_000 }, (_, index) => `cacheable${index}`).join(" ")
1304+
1305+
const generator = claude45Handler.createMessage("", [{ role: "user", content: longUserPrompt }])
1306+
await generator.next()
1307+
1308+
expect(mockConverseStreamCommand).toHaveBeenCalled()
1309+
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
1310+
1311+
expect(findCachePointBlock(commandArg.messages?.[0]?.content)?.cachePoint).toEqual({
1312+
type: "default",
1313+
ttl: "1h",
1314+
})
1315+
})
1316+
12771317
it("should disable prompt caching when awsUsePromptCache is explicitly false", async () => {
12781318
const disabledHandler = new AwsBedrockHandler({
12791319
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",

src/api/providers/bedrock.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { BaseProvider } from "./base-provider"
4343
import { logger } from "../../utils/logging"
4444
import { Package } from "../../shared/package"
4545
import { MultiPointStrategy } from "../transform/cache-strategy/multi-point-strategy"
46-
import { ModelInfo as CacheModelInfo } from "../transform/cache-strategy/types"
46+
import type { ModelInfo as CacheModelInfo, CachePointTtl } from "../transform/cache-strategy/types"
4747
import { convertToBedrockConverseMessages as sharedConverter } from "../transform/bedrock-converse-format"
4848
import { getModelParams } from "../transform/model-params"
4949
import { shouldUseReasoningBudget } from "../../shared/api"
@@ -119,6 +119,16 @@ function normalizeReasoningEffortForBedrock(value: unknown): "low" | "medium" |
119119
return undefined
120120
}
121121

122+
function normalizeBedrockPromptCacheTtl(value: unknown): CachePointTtl | undefined {
123+
return value === "5m" || value === "1h" ? value : undefined
124+
}
125+
126+
function createBedrockCachePointContentBlock(ttl?: CachePointTtl): ContentBlock {
127+
return {
128+
cachePoint: ttl ? { type: "default", ttl } : { type: "default" },
129+
} as unknown as ContentBlock
130+
}
131+
122132
// Extended payload type that includes service_tier as a top-level parameter
123133
// AWS Bedrock service tiers (STANDARD, FLEX, PRIORITY) are specified at the top level
124134
// https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
@@ -959,13 +969,15 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
959969
}
960970

961971
// Convert model info to expected format for cache strategy
972+
const promptCacheTtl = normalizeBedrockPromptCacheTtl(modelInfo?.promptCacheTtl)
962973
const cacheModelInfo: CacheModelInfo = {
963974
maxTokens: modelInfo?.maxTokens || 8192,
964975
contextWindow: modelInfo?.contextWindow || 200_000,
965976
supportsPromptCache: modelInfo?.supportsPromptCache || false,
966977
maxCachePoints: modelInfo?.maxCachePoints || 0,
967978
minTokensPerCachePoint: modelInfo?.minTokensPerCachePoint || 50,
968979
cachableFields: modelInfo?.cachableFields || [],
980+
promptCacheTtl,
969981
}
970982

971983
// Get previous cache point placements for this conversation if available
@@ -998,7 +1010,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
9981010
if (placement) {
9991011
return {
10001012
...msg,
1001-
content: [...(msg.content || []), { cachePoint: { type: "default" } } as ContentBlock],
1013+
content: [...(msg.content || []), createBedrockCachePointContentBlock(promptCacheTtl)],
10021014
}
10031015
}
10041016
return msg

src/api/transform/cache-strategy/__tests__/cache-strategy.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ const hasCachePoint = (block: ContentBlock | SystemContentBlock): boolean => {
4141
)
4242
}
4343

44+
const getCachePoint = (block: ContentBlock | SystemContentBlock): { type?: string; ttl?: string } | undefined => {
45+
if (!("cachePoint" in block) || typeof block.cachePoint !== "object" || block.cachePoint === null) {
46+
return undefined
47+
}
48+
return block.cachePoint as { type?: string; ttl?: string }
49+
}
50+
4451
// Create a mock object to store the last config passed to convertToBedrockConverseMessages
4552
interface CacheConfig {
4653
modelInfo: any
@@ -176,6 +183,25 @@ describe("Cache Strategy", () => {
176183
expect(hasCachePoint(result.system[1])).toBe(true)
177184
})
178185

186+
it("adds configured ttl to system cache blocks", () => {
187+
const shortSystemPrompt = "You are a helpful assistant"
188+
189+
const config = createConfig({
190+
messages: [{ role: "user", content: "Hello" }],
191+
systemPrompt: shortSystemPrompt,
192+
modelInfo: {
193+
...defaultModelInfo,
194+
minTokensPerCachePoint: 1,
195+
promptCacheTtl: "1h",
196+
},
197+
})
198+
199+
const strategy = new MultiPointStrategy(config)
200+
const result = strategy.determineOptimalCachePoints()
201+
202+
expect(getCachePoint(result.system[1])).toMatchObject({ type: "default", ttl: "1h" })
203+
})
204+
179205
it("does not add system cache block when system prompt is too short", () => {
180206
const shortSystemPrompt = "You are a helpful assistant"
181207

@@ -660,6 +686,11 @@ describe("Cache Strategy", () => {
660686
}
661687
}
662688

689+
const createWordMessage = (role: "user" | "assistant", wordCount: number) => ({
690+
role,
691+
content: Array.from({ length: wordCount }, (_, index) => `word${index}`).join(" "),
692+
})
693+
663694
// Helper to log cache point placements for debugging
664695
const logPlacements = (placements: any[]) => {
665696
console.log(
@@ -669,6 +700,70 @@ describe("Cache Strategy", () => {
669700
}
670701

671702
describe("Example 1: Initial Cache Point Placement", () => {
703+
it("should place a cache point after a single first-turn user message that meets the threshold", () => {
704+
const messages = [createWordMessage("user", 60)]
705+
706+
const config = createConfig({
707+
modelInfo: multiPointModelInfo,
708+
systemPrompt: "",
709+
messages,
710+
usePromptCache: true,
711+
})
712+
713+
const strategy = new MultiPointStrategy(config)
714+
const result = strategy.determineOptimalCachePoints()
715+
716+
expect(result.messageCachePointPlacements).toEqual([
717+
expect.objectContaining({
718+
index: 0,
719+
type: "message",
720+
}),
721+
])
722+
expect(result.messageCachePointPlacements?.[0].tokensCovered).toBeGreaterThanOrEqual(
723+
multiPointModelInfo.minTokensPerCachePoint,
724+
)
725+
expect(result.messages[0].content?.some((block) => hasCachePoint(block))).toBe(true)
726+
})
727+
728+
it("should add a cache point for a single new user message after a previous placement", () => {
729+
const messages = [
730+
createWordMessage("user", 60),
731+
createWordMessage("assistant", 5),
732+
createWordMessage("user", 60),
733+
]
734+
735+
const previousCachePointPlacements: CachePointPlacement[] = [
736+
{
737+
index: 0,
738+
type: "message",
739+
tokensCovered: 88,
740+
},
741+
]
742+
743+
const config = createConfig({
744+
modelInfo: multiPointModelInfo,
745+
systemPrompt: "",
746+
messages,
747+
usePromptCache: true,
748+
previousCachePointPlacements,
749+
})
750+
751+
const strategy = new MultiPointStrategy(config)
752+
const result = strategy.determineOptimalCachePoints()
753+
754+
expect(result.messageCachePointPlacements).toEqual([
755+
expect.objectContaining({
756+
index: 0,
757+
type: "message",
758+
}),
759+
expect.objectContaining({
760+
index: 2,
761+
type: "message",
762+
}),
763+
])
764+
expect(result.messages[2].content?.some((block) => hasCachePoint(block))).toBe(true)
765+
})
766+
672767
it("should place a cache point after the second user message", () => {
673768
// Create messages matching Example 1 from documentation
674769
const messages = [

src/api/transform/cache-strategy/base-strategy.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import { ContentBlock, SystemContentBlock, Message, ConversationRole } from "@aws-sdk/client-bedrock-runtime"
3-
import { CacheStrategyConfig, CacheResult, CachePointPlacement } from "./types"
3+
import { CacheStrategyConfig, CacheResult, CachePointPlacement, CachePoint } from "./types"
44

55
export abstract class CacheStrategy {
66
/**
@@ -51,7 +51,13 @@ export abstract class CacheStrategy {
5151
* Create a cache point content block
5252
*/
5353
protected createCachePoint(): ContentBlock {
54-
return { cachePoint: { type: "default" } } as unknown as ContentBlock
54+
const cachePoint: CachePoint = { type: "default" }
55+
56+
if (this.config.modelInfo.promptCacheTtl) {
57+
cachePoint.ttl = this.config.modelInfo.promptCacheTtl
58+
}
59+
60+
return { cachePoint } as unknown as ContentBlock
5561
}
5662

5763
/**

src/api/transform/cache-strategy/multi-point-strategy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class MultiPointStrategy extends CacheStrategy {
6464
minTokensPerPoint: number,
6565
remainingCachePoints: number,
6666
): CachePointPlacement[] {
67-
if (this.config.messages.length <= 1) {
67+
if (remainingCachePoints <= 0) {
6868
return []
6969
}
7070

@@ -250,7 +250,7 @@ export class MultiPointStrategy extends CacheStrategy {
250250
endIndex: number,
251251
minTokensPerPoint: number,
252252
): CachePointPlacement | null {
253-
if (startIndex >= endIndex) {
253+
if (startIndex > endIndex) {
254254
return null
255255
}
256256

src/api/transform/cache-strategy/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { SystemContentBlock, Message } from "@aws-sdk/client-bedrock-runtime"
44
/**
55
* Information about a model's capabilities and constraints
66
*/
7+
export type CachePointTtl = "5m" | "1h"
8+
79
export interface ModelInfo {
810
/** Maximum number of tokens the model can generate */
911
maxTokens: number
@@ -17,6 +19,8 @@ export interface ModelInfo {
1719
minTokensPerCachePoint: number
1820
/** Fields that can be cached */
1921
cachableFields: Array<"system" | "messages" | "tools">
22+
/** Optional Bedrock cachePoint TTL to request */
23+
promptCacheTtl?: CachePointTtl
2024
}
2125

2226
/**
@@ -25,6 +29,8 @@ export interface ModelInfo {
2529
export interface CachePoint {
2630
/** Type of cache point */
2731
type: "default"
32+
/** Optional Bedrock cachePoint TTL */
33+
ttl?: CachePointTtl
2834
}
2935

3036
/**

0 commit comments

Comments
 (0)