Skip to content

Commit 7984393

Browse files
feat(cache): add optional 1h TTL on first system cache marker behind OPENCODE_EXPERIMENTAL_CACHE_1H_TTL flag
1 parent 23df869 commit 7984393

File tree

3 files changed

+86
-4
lines changed

3 files changed

+86
-4
lines changed

packages/opencode/src/flag/flag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export namespace Flag {
5959
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
6060
export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN")
6161
export const OPENCODE_EXPERIMENTAL_CACHE_STABILIZATION = truthy("OPENCODE_EXPERIMENTAL_CACHE_STABILIZATION")
62+
export const OPENCODE_EXPERIMENTAL_CACHE_1H_TTL = truthy("OPENCODE_EXPERIMENTAL_CACHE_1H_TTL")
6263
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
6364
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
6465

packages/opencode/src/provider/transform.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,12 @@ export namespace ProviderTransform {
171171
return msgs
172172
}
173173

174-
function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
174+
function applyCaching(msgs: ModelMessage[], model: Provider.Model, extendedTTL?: boolean): ModelMessage[] {
175175
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
176176
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
177177

178+
// Use 1h cache TTL on first system block (2x write cost vs 1.25x for default 5-min)
179+
const anthropicCache = extendedTTL ? { type: "ephemeral", ttl: "1h" } : { type: "ephemeral" }
178180
const providerOptions = {
179181
anthropic: {
180182
cacheControl: { type: "ephemeral" },
@@ -194,18 +196,21 @@ export namespace ProviderTransform {
194196
}
195197

196198
for (const msg of unique([...system, ...final])) {
199+
const options = msg === system[0]
200+
? { ...providerOptions, anthropic: { cacheControl: anthropicCache } }
201+
: providerOptions
197202
const useMessageLevelOptions = model.providerID === "anthropic" || model.providerID.includes("bedrock")
198203
const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
199204

200205
if (shouldUseContentOptions) {
201206
const lastContent = msg.content[msg.content.length - 1]
202207
if (lastContent && typeof lastContent === "object") {
203-
lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
208+
lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, options)
204209
continue
205210
}
206211
}
207212

208-
msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
213+
msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, options)
209214
}
210215

211216
return msgs
@@ -261,7 +266,7 @@ export namespace ProviderTransform {
261266
model.api.npm === "@ai-sdk/anthropic") &&
262267
model.api.npm !== "@ai-sdk/gateway"
263268
) {
264-
msgs = applyCaching(msgs, model)
269+
msgs = applyCaching(msgs, model, (options.extendedTTL as boolean) ?? Flag.OPENCODE_EXPERIMENTAL_CACHE_1H_TTL)
265270
}
266271

267272
// Remap providerOptions keys from stored providerID to expected SDK key

packages/opencode/test/provider/transform.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,82 @@ describe("ProviderTransform.message - claude w/bedrock custom inference profile"
15311531
})
15321532

15331533

1534+
describe("ProviderTransform.message - first system block gets 1h TTL when flag set", () => {
1535+
const anthropicModel = {
1536+
id: "anthropic/claude-sonnet-4-6",
1537+
providerID: "anthropic",
1538+
api: {
1539+
id: "claude-sonnet-4-6",
1540+
url: "https://api.anthropic.com",
1541+
npm: "@ai-sdk/anthropic",
1542+
},
1543+
capabilities: {
1544+
temperature: true,
1545+
reasoning: false,
1546+
attachment: true,
1547+
toolcall: true,
1548+
input: { text: true, audio: false, image: true, video: false, pdf: true },
1549+
output: { text: true, audio: false, image: false, video: false, pdf: false },
1550+
interleaved: false,
1551+
},
1552+
cost: { input: 0.003, output: 0.015, cache: { read: 0.0003, write: 0.00375 } },
1553+
limit: { context: 200000, output: 8192 },
1554+
status: "active",
1555+
options: {},
1556+
headers: {},
1557+
} as any
1558+
1559+
test("first system block gets 1h TTL when extendedTTL is true", () => {
1560+
const msgs = [
1561+
{ role: "system", content: "Block 1" },
1562+
{ role: "system", content: "Block 2" },
1563+
{ role: "user", content: "Hello" },
1564+
] as any[]
1565+
1566+
const result = ProviderTransform.message(msgs, anthropicModel, { extendedTTL: true }) as any[]
1567+
1568+
expect(result[0].providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral", ttl: "1h" })
1569+
})
1570+
1571+
test("first system block gets default ephemeral when extendedTTL is not set", () => {
1572+
const msgs = [
1573+
{ role: "system", content: "Block 1" },
1574+
{ role: "system", content: "Block 2" },
1575+
{ role: "user", content: "Hello" },
1576+
] as any[]
1577+
1578+
const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
1579+
1580+
expect(result[0].providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral" })
1581+
})
1582+
1583+
test("second system block always gets default ephemeral TTL", () => {
1584+
const msgs = [
1585+
{ role: "system", content: "Block 1" },
1586+
{ role: "system", content: "Block 2" },
1587+
{ role: "user", content: "Hello" },
1588+
] as any[]
1589+
1590+
const result = ProviderTransform.message(msgs, anthropicModel, { extendedTTL: true }) as any[]
1591+
1592+
expect(result[1].providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral" })
1593+
})
1594+
1595+
test("conversation messages get default ephemeral TTL even with extendedTTL", () => {
1596+
const msgs = [
1597+
{ role: "system", content: "System" },
1598+
{ role: "user", content: "Hello" },
1599+
{ role: "assistant", content: "Hi" },
1600+
{ role: "user", content: "World" },
1601+
] as any[]
1602+
1603+
const result = ProviderTransform.message(msgs, anthropicModel, { extendedTTL: true }) as any[]
1604+
1605+
const last = result[result.length - 1]
1606+
expect(last.providerOptions.anthropic.cacheControl).toEqual({ type: "ephemeral" })
1607+
})
1608+
})
1609+
15341610
describe("ProviderTransform.message - cache control on gateway", () => {
15351611
const createModel = (overrides: Partial<any> = {}) =>
15361612
({

0 commit comments

Comments
 (0)