@librechat/agents 3.2.39 → 3.2.42

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.
Files changed (59) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +4 -1
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +4 -3
  5. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  6. package/dist/cjs/llm/bedrock/index.cjs +9 -1
  7. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +4 -0
  9. package/dist/cjs/messages/cache.cjs +21 -0
  10. package/dist/cjs/messages/cache.cjs.map +1 -1
  11. package/dist/cjs/tools/ReadFile.cjs +2 -2
  12. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  13. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +13 -13
  14. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +87 -7
  16. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  17. package/dist/cjs/tools/local/LocalCodingTools.cjs +11 -11
  18. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  19. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +5 -2
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +4 -3
  23. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  24. package/dist/esm/llm/bedrock/index.mjs +10 -2
  25. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  26. package/dist/esm/main.mjs +3 -3
  27. package/dist/esm/messages/cache.mjs +21 -1
  28. package/dist/esm/messages/cache.mjs.map +1 -1
  29. package/dist/esm/tools/ReadFile.mjs +2 -2
  30. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  31. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +14 -14
  32. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  33. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +85 -8
  34. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  35. package/dist/esm/tools/local/LocalCodingTools.mjs +11 -11
  36. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  37. package/dist/types/llm/bedrock/index.d.ts +7 -0
  38. package/dist/types/messages/cache.d.ts +17 -0
  39. package/dist/types/tools/ReadFile.d.ts +4 -4
  40. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +44 -0
  41. package/package.json +1 -1
  42. package/src/agents/AgentContext.ts +2 -0
  43. package/src/graphs/Graph.ts +17 -5
  44. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +12 -12
  45. package/src/hooks/createWorkspacePolicyHook.ts +7 -6
  46. package/src/llm/bedrock/index.ts +18 -2
  47. package/src/llm/bedrock/llm.spec.ts +97 -0
  48. package/src/messages/cache.test.ts +31 -0
  49. package/src/messages/cache.ts +25 -0
  50. package/src/tools/ReadFile.ts +2 -2
  51. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +131 -0
  52. package/src/tools/__tests__/LocalExecutionTools.test.ts +25 -25
  53. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +5 -5
  54. package/src/tools/__tests__/ReadFile.test.ts +3 -3
  55. package/src/tools/__tests__/ToolNode.session.test.ts +2 -2
  56. package/src/tools/__tests__/workspaceSeam.test.ts +2 -2
  57. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +18 -13
  58. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +165 -9
  59. package/src/tools/local/LocalCodingTools.ts +14 -14
@@ -47,6 +47,23 @@ type BedrockCachePoint = {
47
47
  * legacy default), `'1h'` adds the extended-cache `ttl`.
48
48
  */
49
49
  export declare function buildBedrockCachePoint(ttl?: PromptCacheTtl): BedrockCachePoint;
50
+ /**
51
+ * A `cachePoint` under `toolConfig.tools` is only accepted on Anthropic Claude
52
+ * models. Amazon Nova rejects it outright — `Malformed input request:
53
+ * #/toolConfig/tools/0: extraneous key [cachePoint] is not permitted` — even
54
+ * though it accepts `cachePoint` in `system` and `messages` (verified live:
55
+ * Nova returns HTTP 200 with real `cacheWriteInputTokens` for both). Per the AWS
56
+ * prompt-caching docs the support table lists `tools` for Claude only.
57
+ *
58
+ * Gate ONLY the tool checkpoint on this, so a `promptCache: true` config on Nova
59
+ * keeps its valid message/system caching instead of either 400-ing (tool point)
60
+ * or losing caching entirely. This is a capability gate (the key is rejected
61
+ * outright), distinct from the TTL value which Bedrock downgrades gracefully.
62
+ *
63
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
64
+ * @see https://github.com/danny-avila/LibreChat/issues/13838
65
+ */
66
+ export declare function supportsBedrockToolCache(model: string | undefined | null): boolean;
50
67
  /**
51
68
  * Clones a message with new content. For LangChain BaseMessage instances,
52
69
  * constructs a proper class instance so that `instanceof` checks are preserved
@@ -4,12 +4,12 @@ export declare const ReadFileToolDescription = "Read the contents of a file. Ret
4
4
  export declare const ReadFileToolSchema: {
5
5
  readonly type: "object";
6
6
  readonly properties: {
7
- readonly file_path: {
7
+ readonly path: {
8
8
  readonly type: "string";
9
9
  readonly description: "Path to the file. For skill files: \"{skillName}/{path}\" (e.g. \"pdf-analyzer/src/utils.py\"). For code execution output: the path as returned by the execution tool.";
10
10
  };
11
11
  };
12
- readonly required: readonly ["file_path"];
12
+ readonly required: readonly ["path"];
13
13
  };
14
14
  export declare const ReadFileToolDefinition: {
15
15
  readonly name: Constants.READ_FILE;
@@ -17,12 +17,12 @@ export declare const ReadFileToolDefinition: {
17
17
  readonly parameters: {
18
18
  readonly type: "object";
19
19
  readonly properties: {
20
- readonly file_path: {
20
+ readonly path: {
21
21
  readonly type: "string";
22
22
  readonly description: "Path to the file. For skill files: \"{skillName}/{path}\" (e.g. \"pdf-analyzer/src/utils.py\"). For code execution output: the path as returned by the execution tool.";
23
23
  };
24
24
  };
25
- readonly required: readonly ["file_path"];
25
+ readonly required: readonly ["path"];
26
26
  };
27
27
  readonly responseFormat: "content_and_artifact";
28
28
  };
@@ -8,6 +8,50 @@ type SpawnResult = {
8
8
  };
9
9
  export declare function getCloudflareWorkspaceRoot(config?: t.CloudflareSandboxExecutionConfig): string;
10
10
  export declare function resolveCloudflareSandbox(config: t.CloudflareSandboxExecutionConfig): Promise<t.CloudflareSandboxRuntime>;
11
+ /**
12
+ * Client-side backstop timeout for a `sandbox.exec()` await: a few seconds beyond
13
+ * the exec's own `timeout` option, so a stalled exec that never honors `timeout`
14
+ * still can't outlast this.
15
+ */
16
+ export declare function clientExecTimeoutMs(timeoutMs: number): number;
17
+ /**
18
+ * Bound a `sandbox.exec()` await with a CLIENT-SIDE timeout.
19
+ *
20
+ * The native Cloudflare Sandbox Durable Object `exec()` is effectively
21
+ * uncancellable from the host: `ExecOptions` has no `signal` (so
22
+ * `supportsExecSignal` is false for the native transport), and its `timeout`
23
+ * option is not reliably enforced when the container/RPC itself stalls — while
24
+ * the in-sandbox `timeout(1)` wrapper only bounds a command that is actually
25
+ * running. So a stalled exec (an unresponsive/cold container) otherwise hangs
26
+ * until the host's run-level abort, burning the whole run budget on one tool
27
+ * call. This race guarantees the host await settles within `timeoutMs`
28
+ * regardless of the transport.
29
+ *
30
+ * On timeout the underlying `exec` promise may keep running in the DO (a
31
+ * native-DO exec cannot be truly cancelled), so its late settlement is swallowed
32
+ * to avoid an unhandled rejection.
33
+ */
34
+ export declare function withClientTimeout<T>(exec: Promise<T>, timeoutMs: number, label: string, options?: {
35
+ /**
36
+ * Detach the backstop timer from the event loop. Use ONLY when something else
37
+ * already settles the caller (e.g. the spawn path, where spawnLocalProcess's
38
+ * own timer resolves the child). The awaited direct-exec paths must leave it
39
+ * REF'd so the timeout is guaranteed to fire even if nothing else is pending.
40
+ */
41
+ unref?: boolean;
42
+ /** Invoked when the client timeout fires — e.g. abort a signal-aware exec. */
43
+ onTimeout?: () => void;
44
+ }): Promise<T>;
45
+ /**
46
+ * Run `sandbox.exec()` bounded by a client-side timeout, and — for signal-aware
47
+ * transports (e.g. the HTTP bridge, `supportsExecSignal === true`) — abort the
48
+ * underlying exec when the timeout fires instead of merely abandoning it. The
49
+ * native DO transport ignores `signal`, so it only gets the timeout. Leaves the
50
+ * backstop timer ref'd (this is an awaited direct-exec path).
51
+ */
52
+ export declare function execWithClientTimeout(sandbox: t.CloudflareSandboxRuntime, command: string, options: t.CloudflareSandboxExecOptions, timeoutMs: number, label: string, runOptions?: {
53
+ unref?: boolean;
54
+ }): Promise<t.CloudflareSandboxExecResult>;
11
55
  export declare function createCloudflareWorkspaceFS(config: t.CloudflareSandboxExecutionConfig): WorkspaceFS;
12
56
  export declare function createCloudflareLocalExecutionConfig(config: t.CloudflareSandboxExecutionConfig): t.LocalExecutionConfig;
13
57
  export declare function validateCloudflareBashCommand(command: string, args: readonly string[], config: t.CloudflareSandboxExecutionConfig): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.39",
3
+ "version": "3.2.42",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -846,6 +846,8 @@ export class AgentContext {
846
846
  const bedrockOptions = this.clientOptions as
847
847
  | t.BedrockAnthropicClientOptions
848
848
  | undefined;
849
+ // Nova accepts system/message cachePoints (only the tool checkpoint is
850
+ // Claude-only), so this is gated on promptCache alone.
849
851
  return bedrockOptions?.promptCache === true;
850
852
  }
851
853
 
@@ -28,6 +28,7 @@ import {
28
28
  syncBudgetDerivedFields,
29
29
  addTailCacheControl,
30
30
  resolvePromptCacheTtl,
31
+ supportsBedrockToolCache,
31
32
  getMessageId,
32
33
  makeIsDeferred,
33
34
  partitionAndMarkAnthropicToolCache,
@@ -1442,11 +1443,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1442
1443
  | undefined
1443
1444
  )?.promptCache === true
1444
1445
  ) {
1445
- toolsForBinding =
1446
- partitionAndMarkBedrockToolCache(
1447
- rawToolsForBinding,
1448
- makeIsDeferred(agentContext.toolDefinitions)
1449
- ) ?? rawToolsForBinding;
1446
+ const bedrockModel = (
1447
+ agentContext.clientOptions as { model?: string } | undefined
1448
+ )?.model;
1449
+ // An omitted model falls back to LangChain's default Claude model (which
1450
+ // supports tool caching); only an explicit non-Claude model (e.g. Nova)
1451
+ // skips tool marking so its stray marker never leaks into toolConfig.
1452
+ if (bedrockModel == null || supportsBedrockToolCache(bedrockModel)) {
1453
+ toolsForBinding =
1454
+ partitionAndMarkBedrockToolCache(
1455
+ rawToolsForBinding,
1456
+ makeIsDeferred(agentContext.toolDefinitions)
1457
+ ) ?? rawToolsForBinding;
1458
+ }
1450
1459
  }
1451
1460
 
1452
1461
  let model =
@@ -1790,6 +1799,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1790
1799
  | t.ProviderOptionsMap[Providers.OPENROUTER]
1791
1800
  | undefined
1792
1801
  )?.promptCache === true;
1802
+ // Message/system cache points work on all cache-capable Bedrock models,
1803
+ // including Nova (verified live: HTTP 200 with cacheWriteInputTokens). Only
1804
+ // the tool checkpoint is Claude-only, so this is gated on promptCache alone.
1793
1805
  const bedrockPromptCacheEnabled =
1794
1806
  agentContext.provider === Providers.BEDROCK &&
1795
1807
  (
@@ -51,7 +51,7 @@ describe('createWorkspacePolicyHook', () => {
51
51
  const hook = createWorkspacePolicyHook({ root: workspace });
52
52
  const out = await call(
53
53
  hook,
54
- makeInput('read_file', { file_path: 'src/x.ts' })
54
+ makeInput('read_file', { path: 'src/x.ts' })
55
55
  );
56
56
  expect(out.decision).toBe('allow');
57
57
  });
@@ -60,7 +60,7 @@ describe('createWorkspacePolicyHook', () => {
60
60
  const hook = createWorkspacePolicyHook({ root: workspace });
61
61
  const out = await call(
62
62
  hook,
63
- makeInput('read_file', { file_path: join(workspace, 'src/x.ts') })
63
+ makeInput('read_file', { path: join(workspace, 'src/x.ts') })
64
64
  );
65
65
  expect(out.decision).toBe('allow');
66
66
  });
@@ -72,7 +72,7 @@ describe('createWorkspacePolicyHook', () => {
72
72
  });
73
73
  const out = await call(
74
74
  hook,
75
- makeInput('read_file', { file_path: join(extra, 'lib/y.ts') })
75
+ makeInput('read_file', { path: join(extra, 'lib/y.ts') })
76
76
  );
77
77
  expect(out.decision).toBe('allow');
78
78
  });
@@ -81,7 +81,7 @@ describe('createWorkspacePolicyHook', () => {
81
81
  const hook = createWorkspacePolicyHook({ root: workspace });
82
82
  const out = await call(
83
83
  hook,
84
- makeInput('read_file', { file_path: '/etc/passwd' })
84
+ makeInput('read_file', { path: '/etc/passwd' })
85
85
  );
86
86
  expect(out.decision).toBe('ask');
87
87
  expect(out.reason).toContain('/etc/passwd');
@@ -93,7 +93,7 @@ describe('createWorkspacePolicyHook', () => {
93
93
  const out = await call(
94
94
  hook,
95
95
  makeInput('write_file', {
96
- file_path: '/etc/foo',
96
+ path: '/etc/foo',
97
97
  content: 'malicious',
98
98
  })
99
99
  );
@@ -107,7 +107,7 @@ describe('createWorkspacePolicyHook', () => {
107
107
  });
108
108
  const out = await call(
109
109
  hook,
110
- makeInput('read_file', { file_path: '/etc/passwd' })
110
+ makeInput('read_file', { path: '/etc/passwd' })
111
111
  );
112
112
  expect(out.decision).toBe('deny');
113
113
  expect(out.reason).toContain('/etc/passwd');
@@ -121,7 +121,7 @@ describe('createWorkspacePolicyHook', () => {
121
121
  const out = await call(
122
122
  hook,
123
123
  makeInput('edit_file', {
124
- file_path: '/etc/foo',
124
+ path: '/etc/foo',
125
125
  old_text: 'a',
126
126
  new_text: 'b',
127
127
  })
@@ -136,7 +136,7 @@ describe('createWorkspacePolicyHook', () => {
136
136
  });
137
137
  const out = await call(
138
138
  hook,
139
- makeInput('read_file', { file_path: '/etc/passwd' })
139
+ makeInput('read_file', { path: '/etc/passwd' })
140
140
  );
141
141
  expect(out.decision).toBe('allow');
142
142
  });
@@ -184,7 +184,7 @@ describe('createWorkspacePolicyHook', () => {
184
184
  });
185
185
  const out = await call(
186
186
  hook,
187
- makeInput('read_file', { file_path: '/somewhere/else.ts' })
187
+ makeInput('read_file', { path: '/somewhere/else.ts' })
188
188
  );
189
189
  expect(out.reason).toBe('read_file blocked from /somewhere/else.ts');
190
190
  });
@@ -212,7 +212,7 @@ describe('createWorkspacePolicyHook', () => {
212
212
  const hook = createWorkspacePolicyHook({ root: '.' });
213
213
  const out = await call(
214
214
  hook,
215
- makeInput('read_file', { file_path: resolve('.', 'src/x.ts') })
215
+ makeInput('read_file', { path: resolve('.', 'src/x.ts') })
216
216
  );
217
217
  expect(out.decision).toBe('allow');
218
218
  });
@@ -228,7 +228,7 @@ describe('createWorkspacePolicyHook', () => {
228
228
  });
229
229
  const out = await call(
230
230
  hook,
231
- makeInput('read_file', { file_path: 'escape' })
231
+ makeInput('read_file', { path: 'escape' })
232
232
  );
233
233
  expect(out.decision).toBe('deny');
234
234
  expect(out.reason).toContain('escape');
@@ -245,7 +245,7 @@ describe('createWorkspacePolicyHook', () => {
245
245
  });
246
246
  const out = await call(
247
247
  hook,
248
- makeInput('read_file', { file_path: join(altMount, 'file.ts') })
248
+ makeInput('read_file', { path: join(altMount, 'file.ts') })
249
249
  );
250
250
  expect(out.decision).toBe('allow');
251
251
  });
@@ -166,13 +166,14 @@ function extractCompileCheckPaths(input: Record<string, unknown>): string[] {
166
166
  return out;
167
167
  }
168
168
 
169
+ // All built-in coding tools take their file/dir target in `path`.
170
+ const extractPath: PathExtractor = (i) =>
171
+ typeof i.path === 'string' && i.path !== '' ? [i.path] : [];
172
+
169
173
  const DEFAULT_EXTRACTORS: Record<string, PathExtractor> = {
170
- [Constants.READ_FILE]: (i) =>
171
- typeof i.file_path === 'string' ? [i.file_path] : [],
172
- [Constants.WRITE_FILE]: (i) =>
173
- typeof i.file_path === 'string' ? [i.file_path] : [],
174
- [Constants.EDIT_FILE]: (i) =>
175
- typeof i.file_path === 'string' ? [i.file_path] : [],
174
+ [Constants.READ_FILE]: extractPath,
175
+ [Constants.WRITE_FILE]: extractPath,
176
+ [Constants.EDIT_FILE]: extractPath,
176
177
  [Constants.GREP_SEARCH]: (i) =>
177
178
  typeof i.path === 'string' && i.path !== '' ? [i.path] : [],
178
179
  [Constants.GLOB_SEARCH]: (i) =>
@@ -39,7 +39,11 @@ import {
39
39
  handleConverseStreamContentBlockDelta,
40
40
  handleConverseStreamMetadata,
41
41
  } from './utils';
42
- import { resolvePromptCacheTtl, type PromptCacheTtl } from '@/messages/cache';
42
+ import {
43
+ resolvePromptCacheTtl,
44
+ supportsBedrockToolCache,
45
+ type PromptCacheTtl,
46
+ } from '@/messages/cache';
43
47
  import { insertBedrockToolCachePoint } from './toolCache';
44
48
 
45
49
  /**
@@ -134,12 +138,24 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
134
138
  */
135
139
  serviceTier?: ServiceTierType;
136
140
 
141
+ /**
142
+ * The configured model id, captured at construction so it survives the
143
+ * temporary `this.model` swap to an application-inference-profile ARN during
144
+ * generation. Used to gate the Bedrock tool cache point to Claude models
145
+ * (see {@link supportsBedrockToolCache}).
146
+ */
147
+ private readonly cacheModelId: string;
148
+
137
149
  constructor(fields?: CustomChatBedrockConverseInput) {
138
150
  super(fields);
139
151
  this.promptCache = fields?.promptCache;
140
152
  this.promptCacheTtl = fields?.promptCacheTtl;
141
153
  this.applicationInferenceProfile = fields?.applicationInferenceProfile;
142
154
  this.serviceTier = fields?.serviceTier;
155
+ // `super(fields)` initializes `this.model` to LangChain's default Claude
156
+ // model when `fields.model` is omitted, so fall back to it rather than ''
157
+ // (which would treat the default Claude model as tool-cache-unsupported).
158
+ this.cacheModelId = fields?.model ?? this.model;
143
159
  }
144
160
 
145
161
  static lc_name(): string {
@@ -164,7 +180,7 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
164
180
  } {
165
181
  const baseParams = super.invocationParams(options);
166
182
  const toolConfig =
167
- this.promptCache === true
183
+ this.promptCache === true && supportsBedrockToolCache(this.cacheModelId)
168
184
  ? insertBedrockToolCachePoint(
169
185
  baseParams.toolConfig,
170
186
  true,
@@ -375,6 +375,7 @@ describe('CustomChatBedrockConverse', () => {
375
375
  test('adds a Bedrock cache point for directly-bound tools', () => {
376
376
  const model = new CustomChatBedrockConverse({
377
377
  ...baseConstructorArgs,
378
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
378
379
  promptCache: true,
379
380
  });
380
381
 
@@ -400,6 +401,7 @@ describe('CustomChatBedrockConverse', () => {
400
401
  test('defaults the tool cache point to the 1h extended TTL', () => {
401
402
  const model = new CustomChatBedrockConverse({
402
403
  ...baseConstructorArgs,
404
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
403
405
  promptCache: true,
404
406
  });
405
407
 
@@ -430,6 +432,7 @@ describe('CustomChatBedrockConverse', () => {
430
432
  test('honors an explicit 5m promptCacheTtl on the tool cache point', () => {
431
433
  const model = new CustomChatBedrockConverse({
432
434
  ...baseConstructorArgs,
435
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
433
436
  promptCache: true,
434
437
  promptCacheTtl: '5m',
435
438
  });
@@ -461,6 +464,7 @@ describe('CustomChatBedrockConverse', () => {
461
464
  test('adds the Bedrock cache point before deferred tools', () => {
462
465
  const model = new CustomChatBedrockConverse({
463
466
  ...baseConstructorArgs,
467
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
464
468
  promptCache: true,
465
469
  });
466
470
  const tools = partitionAndMarkBedrockToolCache(
@@ -500,6 +504,7 @@ describe('CustomChatBedrockConverse', () => {
500
504
  test('does not fall back to caching when Graph marks all tools deferred', () => {
501
505
  const model = new CustomChatBedrockConverse({
502
506
  ...baseConstructorArgs,
507
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
503
508
  promptCache: true,
504
509
  });
505
510
  const tools = partitionAndMarkBedrockToolCache(
@@ -525,6 +530,98 @@ describe('CustomChatBedrockConverse', () => {
525
530
  '__lc_bedrock_skip_tool_cache'
526
531
  );
527
532
  });
533
+
534
+ test('does not add a tool cache point for Amazon Nova models', () => {
535
+ // Regression for danny-avila/LibreChat#13838: Nova rejects a cachePoint in
536
+ // toolConfig.tools ("Malformed input request: #/toolConfig/tools/0:
537
+ // extraneous key [cachePoint] is not permitted"). Only the tool checkpoint
538
+ // is Claude-only — Nova still caches system/messages — so promptCache: true
539
+ // on Nova must omit just the tool cache point.
540
+ const model = new CustomChatBedrockConverse({
541
+ ...baseConstructorArgs,
542
+ model: 'us.amazon.nova-pro-v1:0',
543
+ promptCache: true,
544
+ });
545
+
546
+ const params = model.invocationParams({
547
+ tools: [
548
+ {
549
+ type: 'function',
550
+ function: {
551
+ name: 'direct_tool',
552
+ description: 'Direct tool',
553
+ parameters: { type: 'object', properties: {} },
554
+ },
555
+ },
556
+ ],
557
+ });
558
+
559
+ expect(getBedrockToolNames(params.toolConfig?.tools)).toEqual([
560
+ 'direct_tool',
561
+ ]);
562
+ expect(JSON.stringify(params.toolConfig?.tools ?? [])).not.toContain(
563
+ 'cachePoint'
564
+ );
565
+ });
566
+
567
+ test('still adds a tool cache point for a Claude model behind an inference profile', () => {
568
+ // The configured Claude model id is captured at construction, so the gate
569
+ // survives the application-inference-profile ARN swap during generation.
570
+ const model = new CustomChatBedrockConverse({
571
+ ...baseConstructorArgs,
572
+ model: 'anthropic.claude-sonnet-4-5-20250929-v1:0',
573
+ applicationInferenceProfile:
574
+ 'arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/test',
575
+ promptCache: true,
576
+ });
577
+
578
+ const params = model.invocationParams({
579
+ tools: [
580
+ {
581
+ type: 'function',
582
+ function: {
583
+ name: 'direct_tool',
584
+ description: 'Direct tool',
585
+ parameters: { type: 'object', properties: {} },
586
+ },
587
+ },
588
+ ],
589
+ });
590
+
591
+ expect(getBedrockToolNames(params.toolConfig?.tools)).toEqual([
592
+ 'direct_tool',
593
+ 'cachePoint',
594
+ ]);
595
+ });
596
+
597
+ test('still adds a tool cache point when model is omitted (default is Claude)', () => {
598
+ // When no model is provided, LangChain initializes this.model to a default
599
+ // Claude model, so the tool cache point must still apply.
600
+ const model = new CustomChatBedrockConverse({
601
+ ...baseConstructorArgs,
602
+ promptCache: true,
603
+ });
604
+
605
+ expect(model.model).toMatch(/claude/i);
606
+
607
+ const params = model.invocationParams({
608
+ tools: [
609
+ {
610
+ type: 'function',
611
+ function: {
612
+ name: 'direct_tool',
613
+ description: 'Direct tool',
614
+ parameters: { type: 'object', properties: {} },
615
+ },
616
+ },
617
+ ],
618
+ });
619
+
620
+ expect(getBedrockToolNames(params.toolConfig?.tools)).toEqual([
621
+ 'direct_tool',
622
+ 'cachePoint',
623
+ ]);
624
+ });
528
625
  });
529
626
 
530
627
  describe('contentBlockIndex cleanup', () => {
@@ -19,6 +19,7 @@ import {
19
19
  buildAnthropicCacheControl,
20
20
  buildBedrockCachePoint,
21
21
  resolvePromptCacheTtl,
22
+ supportsBedrockToolCache,
22
23
  DEFAULT_PROMPT_CACHE_TTL,
23
24
  } from './cache';
24
25
  import { _convertMessagesToOpenAIParams } from '@/llm/openai/utils';
@@ -895,6 +896,36 @@ describe('synthetic skill/meta messages are not cache-anchored', () => {
895
896
  });
896
897
  });
897
898
 
899
+ describe('supportsBedrockToolCache', () => {
900
+ test('returns true for Claude model ids (incl. cross-region + profile)', () => {
901
+ expect(
902
+ supportsBedrockToolCache('anthropic.claude-3-5-sonnet-20241022-v2:0')
903
+ ).toBe(true);
904
+ expect(
905
+ supportsBedrockToolCache('us.anthropic.claude-sonnet-4-5-20250929-v1:0')
906
+ ).toBe(true);
907
+ expect(supportsBedrockToolCache('anthropic.claude-opus-4-6-v1')).toBe(true);
908
+ });
909
+
910
+ test('returns false for Nova and other non-Claude families (tool cache only)', () => {
911
+ // Nova accepts system/message cachePoints but rejects the tool checkpoint.
912
+ expect(supportsBedrockToolCache('us.amazon.nova-pro-v1:0')).toBe(false);
913
+ expect(supportsBedrockToolCache('amazon.nova-lite-v1:0')).toBe(false);
914
+ expect(supportsBedrockToolCache('meta.llama3-1-70b-instruct-v1:0')).toBe(
915
+ false
916
+ );
917
+ expect(supportsBedrockToolCache('mistral.mistral-large-2407-v1:0')).toBe(
918
+ false
919
+ );
920
+ });
921
+
922
+ test('returns false for empty / missing model', () => {
923
+ expect(supportsBedrockToolCache('')).toBe(false);
924
+ expect(supportsBedrockToolCache(undefined)).toBe(false);
925
+ expect(supportsBedrockToolCache(null)).toBe(false);
926
+ });
927
+ });
928
+
898
929
  describe('stripAnthropicCacheControl', () => {
899
930
  it('removes cache_control fields from content blocks', () => {
900
931
  const messages: TestMsg[] = [
@@ -79,6 +79,31 @@ export function buildBedrockCachePoint(
79
79
  return ttl === '1h' ? { type: 'default', ttl: '1h' } : { type: 'default' };
80
80
  }
81
81
 
82
+ /**
83
+ * A `cachePoint` under `toolConfig.tools` is only accepted on Anthropic Claude
84
+ * models. Amazon Nova rejects it outright — `Malformed input request:
85
+ * #/toolConfig/tools/0: extraneous key [cachePoint] is not permitted` — even
86
+ * though it accepts `cachePoint` in `system` and `messages` (verified live:
87
+ * Nova returns HTTP 200 with real `cacheWriteInputTokens` for both). Per the AWS
88
+ * prompt-caching docs the support table lists `tools` for Claude only.
89
+ *
90
+ * Gate ONLY the tool checkpoint on this, so a `promptCache: true` config on Nova
91
+ * keeps its valid message/system caching instead of either 400-ing (tool point)
92
+ * or losing caching entirely. This is a capability gate (the key is rejected
93
+ * outright), distinct from the TTL value which Bedrock downgrades gracefully.
94
+ *
95
+ * @see https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
96
+ * @see https://github.com/danny-avila/LibreChat/issues/13838
97
+ */
98
+ export function supportsBedrockToolCache(
99
+ model: string | undefined | null
100
+ ): boolean {
101
+ if (typeof model !== 'string') {
102
+ return false;
103
+ }
104
+ return /claude|anthropic/i.test(model);
105
+ }
106
+
82
107
  /**
83
108
  * Deep clones a message's content to prevent mutation of the original.
84
109
  */
@@ -22,13 +22,13 @@ CONSTRAINTS:
22
22
  export const ReadFileToolSchema = {
23
23
  type: 'object',
24
24
  properties: {
25
- file_path: {
25
+ path: {
26
26
  type: 'string',
27
27
  description:
28
28
  'Path to the file. For skill files: "{skillName}/{path}" (e.g. "pdf-analyzer/src/utils.py"). For code execution output: the path as returned by the execution tool.',
29
29
  },
30
30
  },
31
- required: ['file_path'],
31
+ required: ['path'],
32
32
  } as const;
33
33
 
34
34
  export const ReadFileToolDefinition = {