@opencode-ai/ai 0.0.0-next-16167 → 0.0.0-next-16169
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cache-policy.js +38 -27
- package/dist/schema/options.js +5 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -207,7 +207,9 @@ Prompt caching is **on by default**. Every `LLMRequest` resolves to `cache: "aut
|
|
|
207
207
|
|
|
208
208
|
### Auto placement
|
|
209
209
|
|
|
210
|
-
`"auto"` places
|
|
210
|
+
`"auto"` places up to four breakpoints — the last tool definition, the first system part, the last system part when distinct, and the final message boundary. These expose successively larger reusable prefixes for tools, the base agent, project instructions, and the active conversation. The rolling final-message boundary is the load-bearing detail in tool loops: it advances on every request so the previous cache entry stays within Anthropic's 20-block lookback.
|
|
211
|
+
|
|
212
|
+
Tools precede every system and conversation block in the provider prefix, so tool definitions must remain byte-stable and deterministically ordered for downstream breakpoints to remain reusable.
|
|
211
213
|
|
|
212
214
|
The math justifies the default: Anthropic's 5-minute cache write is 1.25× base, read is 0.1×, so a single reuse within 5 minutes already wins. One-shot completions below the per-model minimum-cacheable-token threshold silently no-op on the wire, so the worst case is harmless.
|
|
213
215
|
|
|
@@ -235,7 +237,7 @@ cache: {
|
|
|
235
237
|
|
|
236
238
|
### Manual hints
|
|
237
239
|
|
|
238
|
-
Inline `CacheHint` on any text / system / tool / tool-result part overrides automatic placement. The auto policy preserves manual hints
|
|
240
|
+
Inline `CacheHint` on any text / system / tool / tool-result part overrides automatic placement. The auto policy preserves manual hints, counts them against Anthropic and Bedrock's four-breakpoint limit, and only fills the remaining slots.
|
|
239
241
|
|
|
240
242
|
```ts
|
|
241
243
|
LLM.request({
|
|
@@ -251,8 +253,8 @@ LLM.request({
|
|
|
251
253
|
|
|
252
254
|
| Protocol | `cache: "auto"` |
|
|
253
255
|
| ----------------------- | ------------------------------------------------------------------------- |
|
|
254
|
-
| Anthropic Messages | emits up to
|
|
255
|
-
| Bedrock Converse | emits up to
|
|
256
|
+
| Anthropic Messages | emits up to 4 `cache_control` markers (4-breakpoint cap enforced) |
|
|
257
|
+
| Bedrock Converse | emits up to 4 `cachePoint` blocks (4-breakpoint cap enforced) |
|
|
256
258
|
| OpenAI Chat / Responses | no-op (implicit caching above 1024 tokens) |
|
|
257
259
|
| Gemini | no-op (implicit caching on 2.5+; explicit `CachedContent` is out-of-band) |
|
|
258
260
|
|
package/dist/cache-policy.js
CHANGED
|
@@ -2,29 +2,28 @@
|
|
|
2
2
|
// the policy designates. Runs once at compile time, before the per-protocol
|
|
3
3
|
// body builder, so the existing inline-hint lowering path handles the rest.
|
|
4
4
|
//
|
|
5
|
-
// The default `"auto"` shape places
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// assistant/tool round-trips, so caching at that boundary lets every
|
|
11
|
-
// intra-turn API call hit the prefix.
|
|
5
|
+
// The default `"auto"` shape places breakpoints at the last tool definition,
|
|
6
|
+
// the first and last distinct system parts, and the conversation tail. This
|
|
7
|
+
// exposes reusable tool, base-agent, project, and session prefixes while
|
|
8
|
+
// advancing the tail after each tool result keeps the previous cache entry
|
|
9
|
+
// within Anthropic's 20-block lookback during long agent turns.
|
|
12
10
|
//
|
|
13
|
-
// Manual `cache: CacheHint` placements on individual parts are preserved
|
|
14
|
-
//
|
|
11
|
+
// Manual `cache: CacheHint` placements on individual parts are preserved and
|
|
12
|
+
// count against the four-breakpoint budget; auto only fills remaining slots.
|
|
15
13
|
import { CacheHint } from "./schema/options";
|
|
16
14
|
import { LLMRequest, Message, ToolDefinition } from "./schema/messages";
|
|
17
15
|
const AUTO = {
|
|
18
16
|
tools: true,
|
|
19
17
|
system: true,
|
|
20
|
-
messages:
|
|
18
|
+
messages: { tail: 1 },
|
|
21
19
|
};
|
|
22
20
|
const NONE = {};
|
|
21
|
+
const BREAKPOINT_CAP = 4;
|
|
23
22
|
// Resolution rules:
|
|
24
23
|
// - undefined → "auto" — caching is on by default. The math favors it:
|
|
25
24
|
// Anthropic 5m-cache write is 1.25x base, read is 0.1x,
|
|
26
25
|
// so a single reuse within 5 minutes already wins.
|
|
27
|
-
// - "auto" → tools + system +
|
|
26
|
+
// - "auto" → tools + first/last system + final message boundary.
|
|
28
27
|
// - "none" → no auto placement; manual `CacheHint`s still flow.
|
|
29
28
|
// - object form → exactly what the caller asked for.
|
|
30
29
|
const resolve = (policy) => {
|
|
@@ -39,27 +38,33 @@ const resolve = (policy) => {
|
|
|
39
38
|
// whole policy pass for these — emitting hints would be harmless but pointless.
|
|
40
39
|
const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"]);
|
|
41
40
|
const makeHint = (ttlSeconds) => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" });
|
|
42
|
-
const markLastTool = (tools, hint) => {
|
|
41
|
+
const markLastTool = (tools, hint, budget) => {
|
|
43
42
|
if (tools.length === 0)
|
|
44
43
|
return tools;
|
|
45
44
|
const last = tools.length - 1;
|
|
46
|
-
if (tools[last].cache)
|
|
45
|
+
if (tools[last].cache || budget.remaining === 0)
|
|
47
46
|
return tools;
|
|
47
|
+
budget.remaining -= 1;
|
|
48
48
|
return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool));
|
|
49
49
|
};
|
|
50
|
-
const
|
|
50
|
+
const markSystemBoundaries = (system, hint, budget) => {
|
|
51
51
|
if (system.length === 0)
|
|
52
52
|
return system;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
let changed = false;
|
|
54
|
+
const next = system.map((part, index) => {
|
|
55
|
+
if ((index !== 0 && index !== system.length - 1) || part.cache || budget.remaining === 0)
|
|
56
|
+
return part;
|
|
57
|
+
budget.remaining -= 1;
|
|
58
|
+
changed = true;
|
|
59
|
+
return { ...part, cache: hint };
|
|
60
|
+
});
|
|
61
|
+
return changed ? next : system;
|
|
57
62
|
};
|
|
58
63
|
const lastIndexOfRole = (messages, role) => messages.findLastIndex((m) => m.role === role);
|
|
59
64
|
// Mark the last text part of `messages[index]`. If no text part exists, mark
|
|
60
65
|
// the last content part regardless of type — that's the breakpoint position
|
|
61
66
|
// in tool-result-only messages too.
|
|
62
|
-
const markMessageAt = (messages, index, hint) => {
|
|
67
|
+
const markMessageAt = (messages, index, hint, budget) => {
|
|
63
68
|
if (index < 0 || index >= messages.length)
|
|
64
69
|
return messages;
|
|
65
70
|
const target = messages[index];
|
|
@@ -68,8 +73,9 @@ const markMessageAt = (messages, index, hint) => {
|
|
|
68
73
|
const lastTextIndex = target.content.findLastIndex((part) => part.type === "text");
|
|
69
74
|
const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1;
|
|
70
75
|
const existing = target.content[markAt];
|
|
71
|
-
if ("cache" in existing && existing.cache)
|
|
76
|
+
if (("cache" in existing && existing.cache) || budget.remaining === 0)
|
|
72
77
|
return messages;
|
|
78
|
+
budget.remaining -= 1;
|
|
73
79
|
const nextContent = target.content.map((part, i) => (i === markAt ? { ...part, cache: hint } : part));
|
|
74
80
|
const next = new Message({ ...target, content: nextContent });
|
|
75
81
|
// Single pass over `messages`, substituting the one updated entry. Long
|
|
@@ -79,19 +85,23 @@ const markMessageAt = (messages, index, hint) => {
|
|
|
79
85
|
result[index] = next;
|
|
80
86
|
return result;
|
|
81
87
|
};
|
|
82
|
-
const markMessages = (messages, strategy, hint) => {
|
|
88
|
+
const markMessages = (messages, strategy, hint, budget) => {
|
|
83
89
|
if (messages.length === 0)
|
|
84
90
|
return messages;
|
|
85
91
|
if (strategy === "latest-user-message")
|
|
86
|
-
return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint);
|
|
92
|
+
return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget);
|
|
87
93
|
if (strategy === "latest-assistant")
|
|
88
|
-
return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint);
|
|
94
|
+
return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget);
|
|
89
95
|
const start = Math.max(0, messages.length - strategy.tail);
|
|
90
96
|
let next = messages;
|
|
91
97
|
for (let i = start; i < messages.length; i++)
|
|
92
|
-
next = markMessageAt(next, i, hint);
|
|
98
|
+
next = markMessageAt(next, i, hint, budget);
|
|
93
99
|
return next;
|
|
94
100
|
};
|
|
101
|
+
const countHints = (request) => request.tools.reduce((count, tool) => count + (tool.cache === undefined ? 0 : 1), 0) +
|
|
102
|
+
request.system.reduce((count, part) => count + (part.cache === undefined ? 0 : 1), 0) +
|
|
103
|
+
request.messages.reduce((count, message) => count +
|
|
104
|
+
message.content.reduce((contentCount, part) => contentCount + ("cache" in part && part.cache !== undefined ? 1 : 0), 0), 0);
|
|
95
105
|
export const applyCachePolicy = (request) => {
|
|
96
106
|
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id))
|
|
97
107
|
return request;
|
|
@@ -99,9 +109,10 @@ export const applyCachePolicy = (request) => {
|
|
|
99
109
|
if (!policy.tools && !policy.system && !policy.messages)
|
|
100
110
|
return request;
|
|
101
111
|
const hint = makeHint(policy.ttlSeconds);
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
112
|
+
const budget = { remaining: Math.max(0, BREAKPOINT_CAP - countHints(request)) };
|
|
113
|
+
const tools = policy.tools ? markLastTool(request.tools, hint, budget) : request.tools;
|
|
114
|
+
const system = policy.system ? markSystemBoundaries(request.system, hint, budget) : request.system;
|
|
115
|
+
const messages = policy.messages ? markMessages(request.messages, policy.messages, hint, budget) : request.messages;
|
|
105
116
|
if (tools === request.tools && system === request.system && messages === request.messages)
|
|
106
117
|
return request;
|
|
107
118
|
return LLMRequest.update(request, { tools, system, messages });
|
package/dist/schema/options.js
CHANGED
|
@@ -175,11 +175,11 @@ export class CacheHint extends Schema.Class("LLM.CacheHint")({
|
|
|
175
175
|
// Auto-placement policy for prompt caching. The protocol-neutral lowering step
|
|
176
176
|
// reads this and injects `CacheHint`s at the configured boundaries; the
|
|
177
177
|
// per-protocol body builders then translate those hints into wire markers as
|
|
178
|
-
// usual. `"auto"` is the recommended default for agent loops — it places
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
178
|
+
// usual. `"auto"` is the recommended default for agent loops — it places
|
|
179
|
+
// breakpoints at the last tool definition, the first and last distinct system
|
|
180
|
+
// parts, and the conversation tail. The rolling message breakpoint keeps a
|
|
181
|
+
// prior cache entry within Anthropic/Bedrock's 20-block lookback during long
|
|
182
|
+
// tool loops.
|
|
183
183
|
//
|
|
184
184
|
// Pass `"none"` to opt out entirely (the legacy behavior). Pass the granular
|
|
185
185
|
// object form to override individual choices.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-16169",
|
|
4
4
|
"name": "@opencode-ai/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
28
28
|
"@effect/platform-node": "4.0.0-beta.98",
|
|
29
|
-
"@opencode-ai/http-recorder": "0.0.0-next-
|
|
29
|
+
"@opencode-ai/http-recorder": "0.0.0-next-16169",
|
|
30
30
|
"@tsconfig/bun": "1.0.9",
|
|
31
31
|
"@types/bun": "1.3.13",
|
|
32
32
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@smithy/eventstream-codec": "4.2.14",
|
|
37
37
|
"@smithy/util-utf8": "4.2.2",
|
|
38
|
-
"@opencode-ai/schema": "0.0.0-next-
|
|
38
|
+
"@opencode-ai/schema": "0.0.0-next-16169",
|
|
39
39
|
"aws4fetch": "1.0.20",
|
|
40
40
|
"effect": "4.0.0-beta.98",
|
|
41
41
|
"google-auth-library": "10.5.0"
|