@librechat/agents 3.2.38 → 3.2.41
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/dist/cjs/agents/AgentContext.cjs +25 -8
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -4
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +4 -3
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +20 -4
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +7 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/toolCache.cjs +5 -4
- package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +34 -17
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +1 -0
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/toolCache.cjs +18 -5
- package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/messages/anthropicToolCache.cjs +75 -13
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +91 -35
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +3 -2
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +2 -2
- package/dist/cjs/tools/ReadFile.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +11 -11
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +11 -11
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +26 -9
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +8 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +4 -3
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +20 -4
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +7 -1
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/toolCache.mjs +5 -4
- package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +34 -17
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +1 -0
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/toolCache.mjs +18 -5
- package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/anthropicToolCache.mjs +75 -13
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +88 -36
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +4 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +2 -2
- package/dist/esm/tools/ReadFile.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +11 -11
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +11 -11
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +11 -0
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +2 -0
- package/dist/types/llm/bedrock/index.d.ts +13 -0
- package/dist/types/llm/bedrock/toolCache.d.ts +2 -1
- package/dist/types/llm/openrouter/index.d.ts +8 -0
- package/dist/types/llm/openrouter/toolCache.d.ts +2 -1
- package/dist/types/messages/anthropicToolCache.d.ts +2 -1
- package/dist/types/messages/cache.d.ts +49 -5
- package/dist/types/tools/ReadFile.d.ts +4 -4
- package/dist/types/types/llm.d.ts +14 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +64 -17
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +6 -2
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +7 -5
- package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +1 -1
- package/src/agents/__tests__/AgentContext.test.ts +31 -19
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +6 -2
- package/src/graphs/Graph.ts +40 -4
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +12 -12
- package/src/hooks/createWorkspacePolicyHook.ts +7 -6
- package/src/llm/anthropic/utils/message_inputs.ts +33 -6
- package/src/llm/bedrock/index.ts +21 -1
- package/src/llm/bedrock/llm.spec.ts +61 -0
- package/src/llm/bedrock/toolCache.test.ts +24 -0
- package/src/llm/bedrock/toolCache.ts +12 -7
- package/src/llm/bedrock/utils/message_inputs.ts +57 -40
- package/src/llm/openrouter/index.ts +9 -0
- package/src/llm/openrouter/toolCache.test.ts +52 -1
- package/src/llm/openrouter/toolCache.ts +40 -6
- package/src/messages/__tests__/anthropicToolCache.test.ts +168 -0
- package/src/messages/anthropicToolCache.ts +118 -15
- package/src/messages/cache.test.ts +175 -0
- package/src/messages/cache.ts +133 -48
- package/src/summarization/node.ts +21 -2
- package/src/tools/ReadFile.ts +2 -2
- package/src/tools/__tests__/LocalExecutionTools.test.ts +25 -25
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +5 -5
- package/src/tools/__tests__/ReadFile.test.ts +3 -3
- package/src/tools/__tests__/ToolNode.session.test.ts +2 -2
- package/src/tools/__tests__/workspaceSeam.test.ts +2 -2
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +11 -11
- package/src/tools/local/LocalCodingTools.ts +14 -14
- package/src/types/llm.ts +14 -0
|
@@ -140,6 +140,174 @@ describe('partitionAndMarkAnthropicToolCache', () => {
|
|
|
140
140
|
// is returned unchanged (same reference) so we don't churn the array.
|
|
141
141
|
expect(partitionAndMarkAnthropicToolCache(input, () => false)).toBe(input);
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it('stamps the resolved 1h ttl on the last static tool', () => {
|
|
145
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
146
|
+
[fakeTool('a-static'), fakeTool('b-static')] as never,
|
|
147
|
+
() => false,
|
|
148
|
+
'1h'
|
|
149
|
+
) as Array<{
|
|
150
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
151
|
+
}>;
|
|
152
|
+
expect(out[1].extras?.cache_control).toEqual({
|
|
153
|
+
type: 'ephemeral',
|
|
154
|
+
ttl: '1h',
|
|
155
|
+
});
|
|
156
|
+
expect(out[0].extras?.cache_control).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('re-stamps a pre-marked 5m tool to 1h so it does not precede a 1h breakpoint', () => {
|
|
160
|
+
const a = fakeTool('a-static') as {
|
|
161
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
162
|
+
};
|
|
163
|
+
a.extras = { cache_control: { type: 'ephemeral' } };
|
|
164
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
165
|
+
[a] as never,
|
|
166
|
+
() => false,
|
|
167
|
+
'1h'
|
|
168
|
+
) as Array<{
|
|
169
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
170
|
+
}>;
|
|
171
|
+
expect(out[0].extras?.cache_control).toEqual({
|
|
172
|
+
type: 'ephemeral',
|
|
173
|
+
ttl: '1h',
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('strips a pre-marked earlier static tool so only the tail carries the 1h marker', () => {
|
|
178
|
+
const a = fakeTool('a-static') as {
|
|
179
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
180
|
+
};
|
|
181
|
+
a.extras = { cache_control: { type: 'ephemeral' } };
|
|
182
|
+
const b = fakeTool('b-static');
|
|
183
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
184
|
+
[a, b] as never,
|
|
185
|
+
() => false,
|
|
186
|
+
'1h'
|
|
187
|
+
) as Array<{
|
|
188
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
189
|
+
}>;
|
|
190
|
+
// Earlier tool's stray 5m marker is removed so it can't precede the tail.
|
|
191
|
+
expect(out[0].extras?.cache_control).toBeUndefined();
|
|
192
|
+
expect(out[1].extras?.cache_control).toEqual({
|
|
193
|
+
type: 'ephemeral',
|
|
194
|
+
ttl: '1h',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('strips stale markers off deferred tools so they do not precede the system/message breakpoint', () => {
|
|
199
|
+
const staticTool = fakeTool('a-static');
|
|
200
|
+
const deferred = fakeTool('b-deferred') as {
|
|
201
|
+
extras?: { cache_control?: { type: string } };
|
|
202
|
+
};
|
|
203
|
+
deferred.extras = { cache_control: { type: 'ephemeral' } };
|
|
204
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
205
|
+
[staticTool, deferred] as never,
|
|
206
|
+
(name) => name === 'b-deferred',
|
|
207
|
+
'1h'
|
|
208
|
+
) as Array<{
|
|
209
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
210
|
+
}>;
|
|
211
|
+
expect(out[0].extras?.cache_control).toEqual({
|
|
212
|
+
type: 'ephemeral',
|
|
213
|
+
ttl: '1h',
|
|
214
|
+
});
|
|
215
|
+
expect(out[1].extras?.cache_control).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('strips stale markers in the all-deferred case', () => {
|
|
219
|
+
const deferred = fakeTool('only-deferred') as {
|
|
220
|
+
extras?: { cache_control?: { type: string } };
|
|
221
|
+
};
|
|
222
|
+
deferred.extras = { cache_control: { type: 'ephemeral' } };
|
|
223
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
224
|
+
[deferred] as never,
|
|
225
|
+
() => true,
|
|
226
|
+
'1h'
|
|
227
|
+
) as Array<{ extras?: { cache_control?: unknown } }>;
|
|
228
|
+
expect(out[0].extras?.cache_control).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('strips a direct cache_control on an earlier native (non-built-in) tool', () => {
|
|
232
|
+
const nativeWithMarker = {
|
|
233
|
+
name: 'native_a',
|
|
234
|
+
input_schema: { type: 'object', properties: {} },
|
|
235
|
+
cache_control: { type: 'ephemeral' },
|
|
236
|
+
};
|
|
237
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
238
|
+
[nativeWithMarker, fakeTool('native_b')] as never,
|
|
239
|
+
() => false,
|
|
240
|
+
'1h'
|
|
241
|
+
) as Array<{
|
|
242
|
+
cache_control?: unknown;
|
|
243
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
244
|
+
}>;
|
|
245
|
+
expect(out[0].cache_control).toBeUndefined();
|
|
246
|
+
expect(out[1].extras?.cache_control).toEqual({
|
|
247
|
+
type: 'ephemeral',
|
|
248
|
+
ttl: '1h',
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('upgrades a raw Anthropic tail tool to a direct 1h marker (not extras)', () => {
|
|
253
|
+
const nativeTail = {
|
|
254
|
+
name: 'native_tail',
|
|
255
|
+
input_schema: { type: 'object', properties: {} },
|
|
256
|
+
cache_control: { type: 'ephemeral' },
|
|
257
|
+
};
|
|
258
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
259
|
+
[nativeTail] as never,
|
|
260
|
+
() => false,
|
|
261
|
+
'1h'
|
|
262
|
+
) as Array<{
|
|
263
|
+
cache_control?: { type: string; ttl?: string };
|
|
264
|
+
extras?: { cache_control?: unknown };
|
|
265
|
+
}>;
|
|
266
|
+
// Raw Anthropic tools carry cache_control directly (extras is not promoted
|
|
267
|
+
// for them), so the stale marker is upgraded in place to 1h — not moved.
|
|
268
|
+
expect(out[0].cache_control).toEqual({ type: 'ephemeral', ttl: '1h' });
|
|
269
|
+
expect(out[0].extras?.cache_control).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('reorders deferred tools after a correctly pre-marked static tool', () => {
|
|
273
|
+
const deferred = fakeTool('z-deferred');
|
|
274
|
+
const staticTool = fakeTool('a-static') as {
|
|
275
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
276
|
+
};
|
|
277
|
+
// Already carries the resolved 1h marker, so nothing is mutated...
|
|
278
|
+
staticTool.extras = { cache_control: { type: 'ephemeral', ttl: '1h' } };
|
|
279
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
280
|
+
[deferred, staticTool] as never, // deferred precedes static in the input
|
|
281
|
+
(name) => name === 'z-deferred',
|
|
282
|
+
'1h'
|
|
283
|
+
) as Array<{ name?: string }>;
|
|
284
|
+
// ...but the static (cached) tool must still be hoisted ahead of the
|
|
285
|
+
// deferred tool so the breakpoint precedes discovered tools.
|
|
286
|
+
expect(out.map((t) => t.name)).toEqual(['a-static', 'z-deferred']);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('re-stamps a LangChain tool whose marker sits only on the direct block', () => {
|
|
290
|
+
// A StructuredTool (custom) pre-marked directly — not under extras — does
|
|
291
|
+
// not reach the payload, so the breakpoint must be re-stamped under extras.
|
|
292
|
+
const t = fakeTool('a-static') as {
|
|
293
|
+
cache_control?: unknown;
|
|
294
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
295
|
+
};
|
|
296
|
+
t.cache_control = { type: 'ephemeral', ttl: '1h' };
|
|
297
|
+
const out = partitionAndMarkAnthropicToolCache(
|
|
298
|
+
[t] as never,
|
|
299
|
+
() => false,
|
|
300
|
+
'1h'
|
|
301
|
+
) as Array<{
|
|
302
|
+
cache_control?: unknown;
|
|
303
|
+
extras?: { cache_control?: { type: string; ttl?: string } };
|
|
304
|
+
}>;
|
|
305
|
+
expect(out[0].extras?.cache_control).toEqual({
|
|
306
|
+
type: 'ephemeral',
|
|
307
|
+
ttl: '1h',
|
|
308
|
+
});
|
|
309
|
+
expect(out[0].cache_control).toBeUndefined();
|
|
310
|
+
});
|
|
143
311
|
});
|
|
144
312
|
|
|
145
313
|
describe('makeIsDeferred', () => {
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import type { GraphTools } from '@/types';
|
|
29
|
+
import {
|
|
30
|
+
buildAnthropicCacheControl,
|
|
31
|
+
type PromptCacheTtl,
|
|
32
|
+
} from '@/messages/cache';
|
|
29
33
|
|
|
30
34
|
const ANTHROPIC_BUILT_IN_TOOL_PREFIXES = [
|
|
31
35
|
'text_editor_',
|
|
@@ -41,8 +45,6 @@ const ANTHROPIC_BUILT_IN_TOOL_PREFIXES = [
|
|
|
41
45
|
'mcp_toolset',
|
|
42
46
|
] as const;
|
|
43
47
|
|
|
44
|
-
const CACHE_CONTROL = { type: 'ephemeral' as const };
|
|
45
|
-
|
|
46
48
|
type AnthropicToolCacheCandidate = {
|
|
47
49
|
name?: unknown;
|
|
48
50
|
type?: unknown;
|
|
@@ -60,29 +62,91 @@ function isAnthropicBuiltInTool(
|
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Whether a tool already carries a cache breakpoint. Built-ins use a direct
|
|
67
|
+
* `cache_control`; custom tools normally carry it under `extras`, but a
|
|
68
|
+
* caller-supplied Anthropic-native tool object can also put it directly on the
|
|
69
|
+
* block — check both so stale markers are never missed.
|
|
70
|
+
*/
|
|
63
71
|
function hasCacheControl(tool: AnthropicToolCacheCandidate): boolean {
|
|
64
72
|
if (isAnthropicBuiltInTool(tool)) {
|
|
65
73
|
return tool.cache_control != null;
|
|
66
74
|
}
|
|
67
|
-
return tool.extras?.cache_control != null;
|
|
75
|
+
return tool.cache_control != null || tool.extras?.cache_control != null;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Return the `cache_control` from the location that actually reaches the
|
|
80
|
+
* Anthropic payload for this tool's shape: directly on the block for
|
|
81
|
+
* provider-shaped tools (built-ins and raw Anthropic tools), or under `extras`
|
|
82
|
+
* for LangChain custom tools (the adapter promotes only that). Returns
|
|
83
|
+
* `undefined` when no marker sits in the effective location — a marker in the
|
|
84
|
+
* wrong place (e.g. a direct marker on a custom tool) does not count.
|
|
85
|
+
*/
|
|
86
|
+
function getEffectiveCacheControl(
|
|
87
|
+
tool: AnthropicToolCacheCandidate
|
|
88
|
+
): { ttl?: unknown } | undefined {
|
|
89
|
+
return (
|
|
90
|
+
isProviderShapedTool(tool) ? tool.cache_control : tool.extras?.cache_control
|
|
91
|
+
) as { ttl?: unknown } | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return a clone of `tool` with any `cache_control` removed — both the direct
|
|
96
|
+
* block marker and the `extras` marker — preserving the prototype chain. Used
|
|
97
|
+
* to clear stray markers off tools that must not anchor a competing breakpoint.
|
|
98
|
+
*/
|
|
99
|
+
function stripCacheControl(
|
|
71
100
|
tool: AnthropicToolCacheCandidate
|
|
72
101
|
): AnthropicToolCacheCandidate {
|
|
73
102
|
const prototype = Object.getPrototypeOf(tool) ?? Object.prototype;
|
|
74
|
-
|
|
103
|
+
const wrapped = { ...tool };
|
|
104
|
+
delete wrapped.cache_control;
|
|
105
|
+
if (wrapped.extras != null) {
|
|
106
|
+
wrapped.extras = { ...wrapped.extras };
|
|
107
|
+
delete wrapped.extras.cache_control;
|
|
108
|
+
}
|
|
109
|
+
return Object.assign(Object.create(prototype), wrapped);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether `tool` is already in the Anthropic provider payload shape — an
|
|
114
|
+
* Anthropic built-in or a raw Anthropic tool object (has `input_schema`). These
|
|
115
|
+
* carry `cache_control` directly on the block; the LangChain adapter does NOT
|
|
116
|
+
* promote `extras.cache_control` for them. LangChain StructuredTools, by
|
|
117
|
+
* contrast, expose the marker via `extras`.
|
|
118
|
+
*/
|
|
119
|
+
function isProviderShapedTool(tool: AnthropicToolCacheCandidate): boolean {
|
|
120
|
+
return (
|
|
121
|
+
isAnthropicBuiltInTool(tool) ||
|
|
122
|
+
'input_schema' in (tool as Record<string, unknown>)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function markCacheControl(
|
|
127
|
+
tool: AnthropicToolCacheCandidate,
|
|
128
|
+
ttl?: PromptCacheTtl
|
|
129
|
+
): AnthropicToolCacheCandidate {
|
|
130
|
+
const cacheControl = buildAnthropicCacheControl(ttl);
|
|
131
|
+
const prototype = Object.getPrototypeOf(tool) ?? Object.prototype;
|
|
132
|
+
if (isProviderShapedTool(tool)) {
|
|
133
|
+
// Built-ins and raw Anthropic tool objects carry cache_control directly on
|
|
134
|
+
// the block; `extras` is not promoted onto the payload for these shapes.
|
|
75
135
|
const wrapped = { ...tool };
|
|
76
136
|
delete wrapped.extras;
|
|
77
137
|
return Object.assign(Object.create(prototype), wrapped, {
|
|
78
|
-
cache_control:
|
|
138
|
+
cache_control: cacheControl,
|
|
79
139
|
});
|
|
80
140
|
}
|
|
81
141
|
|
|
82
|
-
|
|
142
|
+
// LangChain custom tools: drop any direct marker and expose the breakpoint via
|
|
143
|
+
// `extras`, which the Anthropic adapter promotes onto the payload.
|
|
144
|
+
const wrapped = { ...tool };
|
|
145
|
+
delete wrapped.cache_control;
|
|
146
|
+
return Object.assign(Object.create(prototype), wrapped, {
|
|
83
147
|
extras: {
|
|
84
148
|
...(tool.extras ?? {}),
|
|
85
|
-
cache_control:
|
|
149
|
+
cache_control: cacheControl,
|
|
86
150
|
},
|
|
87
151
|
});
|
|
88
152
|
}
|
|
@@ -124,7 +188,8 @@ export function makeIsDeferred(
|
|
|
124
188
|
*/
|
|
125
189
|
export function partitionAndMarkAnthropicToolCache(
|
|
126
190
|
tools: GraphTools | undefined,
|
|
127
|
-
isDeferred: (toolName: string) => boolean
|
|
191
|
+
isDeferred: (toolName: string) => boolean,
|
|
192
|
+
ttl?: PromptCacheTtl
|
|
128
193
|
): GraphTools | undefined {
|
|
129
194
|
if (tools == null || tools.length === 0) return tools;
|
|
130
195
|
|
|
@@ -143,19 +208,57 @@ export function partitionAndMarkAnthropicToolCache(
|
|
|
143
208
|
}
|
|
144
209
|
}
|
|
145
210
|
|
|
211
|
+
// Anthropic serializes ALL tools before system/messages, so a stray
|
|
212
|
+
// cache_control on any tool — static or deferred — that survives the resolved
|
|
213
|
+
// breakpoint would violate the longer-TTL-first ordering. Strip stale markers
|
|
214
|
+
// off the deferred tools first (they sit after the breakpoint but still before
|
|
215
|
+
// system/messages, and the all-deferred case has no breakpoint of its own).
|
|
216
|
+
let mutated = false;
|
|
217
|
+
for (let i = 0; i < deferredTools.length; i++) {
|
|
218
|
+
const candidate = deferredTools[i] as AnthropicToolCacheCandidate;
|
|
219
|
+
if (hasCacheControl(candidate)) {
|
|
220
|
+
deferredTools[i] = stripCacheControl(candidate);
|
|
221
|
+
mutated = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
146
225
|
if (staticTools.length === 0) {
|
|
147
|
-
return tools;
|
|
226
|
+
return mutated ? ([...deferredTools] as GraphTools) : tools;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Strip any stray cache_control off the earlier static tools so a leftover
|
|
230
|
+
// 5-minute marker never sits ahead of the resolved breakpoint, then stamp (or
|
|
231
|
+
// re-stamp) only the last static tool with the resolved TTL.
|
|
232
|
+
for (let i = 0; i < staticTools.length - 1; i++) {
|
|
233
|
+
const candidate = staticTools[i] as AnthropicToolCacheCandidate;
|
|
234
|
+
if (hasCacheControl(candidate)) {
|
|
235
|
+
staticTools[i] = stripCacheControl(candidate);
|
|
236
|
+
mutated = true;
|
|
237
|
+
}
|
|
148
238
|
}
|
|
149
239
|
|
|
150
240
|
const last = staticTools[
|
|
151
241
|
staticTools.length - 1
|
|
152
242
|
] as AnthropicToolCacheCandidate;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
243
|
+
const desiredTtl: '1h' | undefined = ttl === '1h' ? '1h' : undefined;
|
|
244
|
+
// "Already correct" requires a marker IN the effective location (one that
|
|
245
|
+
// reaches the payload for this tool's shape) carrying the resolved TTL. A
|
|
246
|
+
// marker in the wrong place — e.g. a direct `cache_control` on a LangChain
|
|
247
|
+
// custom tool — is ineffective and must be re-stamped.
|
|
248
|
+
const effective = getEffectiveCacheControl(last);
|
|
249
|
+
const lastAlreadyCorrect =
|
|
250
|
+
effective != null &&
|
|
251
|
+
(effective.ttl === '1h' ? '1h' : undefined) === desiredTtl;
|
|
252
|
+
if (!lastAlreadyCorrect) {
|
|
253
|
+
staticTools[staticTools.length - 1] = markCacheControl(last, ttl);
|
|
254
|
+
mutated = true;
|
|
157
255
|
}
|
|
158
256
|
|
|
159
|
-
|
|
257
|
+
// Return the original reference only when nothing changed AND partitioning
|
|
258
|
+
// moved nothing. When deferred tools exist they must end up after the cache
|
|
259
|
+
// breakpoint, so the partitioned array is returned even if no marker changed.
|
|
260
|
+
if (!mutated && deferredTools.length === 0) {
|
|
261
|
+
return tools;
|
|
262
|
+
}
|
|
160
263
|
return [...staticTools, ...deferredTools] as GraphTools;
|
|
161
264
|
}
|
|
@@ -12,8 +12,14 @@ import {
|
|
|
12
12
|
stripAnthropicCacheControl,
|
|
13
13
|
stripBedrockCacheControl,
|
|
14
14
|
addBedrockCacheControl,
|
|
15
|
+
addBedrockTailCacheControl,
|
|
15
16
|
addCacheControl,
|
|
17
|
+
addTailCacheControl,
|
|
16
18
|
addCacheControlToStablePrefixMessages,
|
|
19
|
+
buildAnthropicCacheControl,
|
|
20
|
+
buildBedrockCachePoint,
|
|
21
|
+
resolvePromptCacheTtl,
|
|
22
|
+
DEFAULT_PROMPT_CACHE_TTL,
|
|
17
23
|
} from './cache';
|
|
18
24
|
import { _convertMessagesToOpenAIParams } from '@/llm/openai/utils';
|
|
19
25
|
import { toLangChainContent } from './langchain';
|
|
@@ -1813,3 +1819,172 @@ describe('OpenRouter prompt caching (reuses addCacheControl)', () => {
|
|
|
1813
1819
|
expect('cache_control' in lastContent[0]).toBe(true);
|
|
1814
1820
|
});
|
|
1815
1821
|
});
|
|
1822
|
+
|
|
1823
|
+
describe('prompt-cache TTL (1h default, 5m legacy)', () => {
|
|
1824
|
+
describe('resolvePromptCacheTtl', () => {
|
|
1825
|
+
it('defaults to the 1h extended cache when unset', () => {
|
|
1826
|
+
expect(resolvePromptCacheTtl(undefined)).toBe('1h');
|
|
1827
|
+
expect(DEFAULT_PROMPT_CACHE_TTL).toBe('1h');
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
it('passes an explicit value through unchanged', () => {
|
|
1831
|
+
expect(resolvePromptCacheTtl('5m')).toBe('5m');
|
|
1832
|
+
expect(resolvePromptCacheTtl('1h')).toBe('1h');
|
|
1833
|
+
});
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
describe('marker builders', () => {
|
|
1837
|
+
it('buildAnthropicCacheControl adds ttl only for 1h', () => {
|
|
1838
|
+
expect(buildAnthropicCacheControl('1h')).toEqual({
|
|
1839
|
+
type: 'ephemeral',
|
|
1840
|
+
ttl: '1h',
|
|
1841
|
+
});
|
|
1842
|
+
// 5m / undefined stay byte-identical to the legacy marker (no ttl)
|
|
1843
|
+
expect(buildAnthropicCacheControl('5m')).toEqual({ type: 'ephemeral' });
|
|
1844
|
+
expect(buildAnthropicCacheControl()).toEqual({ type: 'ephemeral' });
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
it('buildBedrockCachePoint adds ttl only for 1h', () => {
|
|
1848
|
+
expect(buildBedrockCachePoint('1h')).toEqual({
|
|
1849
|
+
type: 'default',
|
|
1850
|
+
ttl: '1h',
|
|
1851
|
+
});
|
|
1852
|
+
expect(buildBedrockCachePoint('5m')).toEqual({ type: 'default' });
|
|
1853
|
+
expect(buildBedrockCachePoint()).toEqual({ type: 'default' });
|
|
1854
|
+
});
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
describe('addTailCacheControl threads ttl', () => {
|
|
1858
|
+
const baseMessages = (): AnthropicMessages => [
|
|
1859
|
+
{ role: 'user', content: 'Hello' },
|
|
1860
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
1861
|
+
];
|
|
1862
|
+
|
|
1863
|
+
it('stamps a 1h cache_control on the tail block', () => {
|
|
1864
|
+
const result = addTailCacheControl(baseMessages(), '1h');
|
|
1865
|
+
const tail = result[result.length - 1].content as MessageContentComplex[];
|
|
1866
|
+
expect(tail[tail.length - 1]).toEqual({
|
|
1867
|
+
type: 'text',
|
|
1868
|
+
text: 'Hi there',
|
|
1869
|
+
cache_control: { type: 'ephemeral', ttl: '1h' },
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
it('omits ttl for 5m and for the unspecified (legacy) default', () => {
|
|
1874
|
+
for (const ttl of ['5m', undefined] as const) {
|
|
1875
|
+
const result = addTailCacheControl(baseMessages(), ttl);
|
|
1876
|
+
const tail = result[result.length - 1]
|
|
1877
|
+
.content as MessageContentComplex[];
|
|
1878
|
+
expect(
|
|
1879
|
+
(tail[tail.length - 1] as Anthropic.TextBlockParam).cache_control
|
|
1880
|
+
).toEqual({ type: 'ephemeral' });
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
describe('addCacheControl threads ttl', () => {
|
|
1886
|
+
it('stamps a 1h cache_control on the latest user messages', () => {
|
|
1887
|
+
const messages: AnthropicMessages = [
|
|
1888
|
+
{ role: 'user', content: 'first' },
|
|
1889
|
+
{ role: 'assistant', content: 'reply' },
|
|
1890
|
+
{ role: 'user', content: 'second' },
|
|
1891
|
+
];
|
|
1892
|
+
const result = addCacheControl(messages, '1h');
|
|
1893
|
+
const lastUser = result[2].content as MessageContentComplex[];
|
|
1894
|
+
expect((lastUser[0] as Anthropic.TextBlockParam).cache_control).toEqual({
|
|
1895
|
+
type: 'ephemeral',
|
|
1896
|
+
ttl: '1h',
|
|
1897
|
+
});
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
describe('addCacheControlToStablePrefixMessages threads ttl', () => {
|
|
1902
|
+
it('stamps 1h on every stable-prefix marker', () => {
|
|
1903
|
+
const messages: AnthropicMessages = [
|
|
1904
|
+
{ role: 'user', content: 'turn 1' },
|
|
1905
|
+
{ role: 'assistant', content: 'reply 1' },
|
|
1906
|
+
{ role: 'user', content: 'turn 2' },
|
|
1907
|
+
{ role: 'assistant', content: 'reply 2' },
|
|
1908
|
+
];
|
|
1909
|
+
const result = addCacheControlToStablePrefixMessages(messages, 2, '1h');
|
|
1910
|
+
const marked = result
|
|
1911
|
+
.flatMap((m) =>
|
|
1912
|
+
Array.isArray(m.content) ? (m.content as MessageContentComplex[]) : []
|
|
1913
|
+
)
|
|
1914
|
+
.filter((block) => 'cache_control' in block);
|
|
1915
|
+
expect(marked.length).toBeGreaterThan(0);
|
|
1916
|
+
for (const block of marked) {
|
|
1917
|
+
expect((block as Anthropic.TextBlockParam).cache_control).toEqual({
|
|
1918
|
+
type: 'ephemeral',
|
|
1919
|
+
ttl: '1h',
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
describe('addBedrockTailCacheControl threads ttl', () => {
|
|
1926
|
+
const messages = (): TestMsg[] => [
|
|
1927
|
+
{ role: 'user', content: 'Hello' },
|
|
1928
|
+
{ role: 'assistant', content: 'Hi' },
|
|
1929
|
+
];
|
|
1930
|
+
|
|
1931
|
+
it('stamps a 1h cachePoint on the tail message', () => {
|
|
1932
|
+
const result = addBedrockTailCacheControl(messages(), '1h');
|
|
1933
|
+
const last = result[result.length - 1].content as MessageContentComplex[];
|
|
1934
|
+
expect(last[last.length - 1]).toEqual({
|
|
1935
|
+
cachePoint: { type: 'default', ttl: '1h' },
|
|
1936
|
+
});
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
it('omits ttl for 5m and the legacy default', () => {
|
|
1940
|
+
for (const ttl of ['5m', undefined] as const) {
|
|
1941
|
+
const result = addBedrockTailCacheControl(messages(), ttl);
|
|
1942
|
+
const last = result[result.length - 1]
|
|
1943
|
+
.content as MessageContentComplex[];
|
|
1944
|
+
expect(last[last.length - 1]).toEqual({
|
|
1945
|
+
cachePoint: { type: 'default' },
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
it('normalizes a stale 5m system cachePoint to the resolved tail ttl', () => {
|
|
1951
|
+
const msgs: TestMsg[] = [
|
|
1952
|
+
{
|
|
1953
|
+
role: 'system',
|
|
1954
|
+
content: [
|
|
1955
|
+
{ type: ContentTypes.TEXT, text: 'System' },
|
|
1956
|
+
{ cachePoint: { type: 'default' } } as MessageContentComplex,
|
|
1957
|
+
],
|
|
1958
|
+
},
|
|
1959
|
+
{ role: 'user', content: 'Hello' },
|
|
1960
|
+
{ role: 'assistant', content: 'Hi' },
|
|
1961
|
+
];
|
|
1962
|
+
const result = addBedrockTailCacheControl(msgs, '1h');
|
|
1963
|
+
// Stale 5m system checkpoint is upgraded to 1h so it never precedes the
|
|
1964
|
+
// 1h message tail (Bedrock requires longer-TTL checkpoints first).
|
|
1965
|
+
const system = result[0].content as MessageContentComplex[];
|
|
1966
|
+
expect(system[system.length - 1]).toEqual({
|
|
1967
|
+
cachePoint: { type: 'default', ttl: '1h' },
|
|
1968
|
+
});
|
|
1969
|
+
const tail = result[result.length - 1].content as MessageContentComplex[];
|
|
1970
|
+
expect(tail[tail.length - 1]).toEqual({
|
|
1971
|
+
cachePoint: { type: 'default', ttl: '1h' },
|
|
1972
|
+
});
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
describe('addBedrockCacheControl threads ttl', () => {
|
|
1977
|
+
it('stamps a 1h cachePoint when configured', () => {
|
|
1978
|
+
const messages: TestMsg[] = [
|
|
1979
|
+
{ role: 'user', content: 'Hello' },
|
|
1980
|
+
{ role: 'assistant', content: 'Hi' },
|
|
1981
|
+
];
|
|
1982
|
+
const result = addBedrockCacheControl(messages, '1h');
|
|
1983
|
+
// Only one user message present, so the cachePoint anchors on it.
|
|
1984
|
+
const user = result[0].content as MessageContentComplex[];
|
|
1985
|
+
expect(user[user.length - 1]).toEqual({
|
|
1986
|
+
cachePoint: { type: 'default', ttl: '1h' },
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
});
|