@librechat/agents 3.2.41 → 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.
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +4 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +9 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/messages/cache.cjs +21 -0
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +87 -7
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +5 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +10 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/cache.mjs +21 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +85 -8
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/types/llm/bedrock/index.d.ts +7 -0
- package/dist/types/messages/cache.d.ts +17 -0
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +44 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +2 -0
- package/src/graphs/Graph.ts +17 -5
- package/src/llm/bedrock/index.ts +18 -2
- package/src/llm/bedrock/llm.spec.ts +97 -0
- package/src/messages/cache.test.ts +31 -0
- package/src/messages/cache.ts +25 -0
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +131 -0
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +7 -2
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +165 -9
|
@@ -105,6 +105,13 @@ export declare class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
105
105
|
* Service tier for model invocation.
|
|
106
106
|
*/
|
|
107
107
|
serviceTier?: ServiceTierType;
|
|
108
|
+
/**
|
|
109
|
+
* The configured model id, captured at construction so it survives the
|
|
110
|
+
* temporary `this.model` swap to an application-inference-profile ARN during
|
|
111
|
+
* generation. Used to gate the Bedrock tool cache point to Claude models
|
|
112
|
+
* (see {@link supportsBedrockToolCache}).
|
|
113
|
+
*/
|
|
114
|
+
private readonly cacheModelId;
|
|
108
115
|
constructor(fields?: CustomChatBedrockConverseInput);
|
|
109
116
|
static lc_name(): string;
|
|
110
117
|
/**
|
|
@@ -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
|
|
@@ -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
|
@@ -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
|
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
(
|
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -39,7 +39,11 @@ import {
|
|
|
39
39
|
handleConverseStreamContentBlockDelta,
|
|
40
40
|
handleConverseStreamMetadata,
|
|
41
41
|
} from './utils';
|
|
42
|
-
import {
|
|
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[] = [
|
package/src/messages/cache.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -2,6 +2,7 @@ import type * as t from '@/types';
|
|
|
2
2
|
import {
|
|
3
3
|
createCloudflareWorkspaceFS,
|
|
4
4
|
createCloudflareLocalExecutionConfig,
|
|
5
|
+
execWithClientTimeout,
|
|
5
6
|
executeCloudflareBash,
|
|
6
7
|
executeCloudflareCode,
|
|
7
8
|
} from '../cloudflare/CloudflareSandboxExecutionEngine';
|
|
@@ -257,6 +258,136 @@ describe('Cloudflare sandbox execution backend', () => {
|
|
|
257
258
|
expect(result.timedOut).toBe(true);
|
|
258
259
|
});
|
|
259
260
|
|
|
261
|
+
it('rejects with a client-side timeout when sandbox exec stalls (no native cancellation)', async () => {
|
|
262
|
+
// The native Cloudflare Sandbox DO exec() is uncancellable (ExecOptions has no
|
|
263
|
+
// signal) and its own `timeout` is not enforced when the container/RPC stalls,
|
|
264
|
+
// while the in-sandbox `timeout(1)` wrapper only bounds a *running* command.
|
|
265
|
+
// Without a client-side race a stalled exec hangs until the host's run-level
|
|
266
|
+
// abort, burning the whole budget on one tool call (issue #251).
|
|
267
|
+
jest.useFakeTimers();
|
|
268
|
+
try {
|
|
269
|
+
let mainExecCalls = 0;
|
|
270
|
+
const sandbox = createRuntime({
|
|
271
|
+
exec: (command) => {
|
|
272
|
+
// Cleanup (`rm -rf`) resolves immediately; the real command stalls,
|
|
273
|
+
// simulating an unresponsive / cold container exec that never returns.
|
|
274
|
+
if (command.startsWith('rm -rf')) {
|
|
275
|
+
return Promise.resolve({ exitCode: 0, stdout: '', stderr: '' });
|
|
276
|
+
}
|
|
277
|
+
mainExecCalls += 1;
|
|
278
|
+
return new Promise<t.CloudflareSandboxExecResult>(() => undefined);
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const promise = executeCloudflareCode(
|
|
283
|
+
{ lang: 'py', code: 'print("slow")' },
|
|
284
|
+
{ sandbox, workspaceRoot: '/workspace', timeoutMs: 1000 }
|
|
285
|
+
);
|
|
286
|
+
const assertion = expect(promise).rejects.toThrow(/client-side timeout/);
|
|
287
|
+
|
|
288
|
+
// Client backstop = outerTimeoutMs(1000) + 5000 = 11000ms; advance past it.
|
|
289
|
+
await jest.advanceTimersByTimeAsync(11500);
|
|
290
|
+
await assertion;
|
|
291
|
+
expect(mainExecCalls).toBe(1);
|
|
292
|
+
} finally {
|
|
293
|
+
jest.useRealTimers();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('aborts signal-aware execs when the client timeout fires', async () => {
|
|
298
|
+
// For signal-aware transports (e.g. the HTTP bridge), a client timeout should
|
|
299
|
+
// actually cancel the underlying exec, not just abandon it.
|
|
300
|
+
jest.useFakeTimers();
|
|
301
|
+
try {
|
|
302
|
+
let mainSignal: AbortSignal | undefined;
|
|
303
|
+
const sandbox = createRuntime({
|
|
304
|
+
supportsExecSignal: true,
|
|
305
|
+
exec: (command, options) => {
|
|
306
|
+
if (command.startsWith('rm -rf')) {
|
|
307
|
+
return Promise.resolve({ exitCode: 0, stdout: '', stderr: '' });
|
|
308
|
+
}
|
|
309
|
+
mainSignal = options?.signal;
|
|
310
|
+
return new Promise<t.CloudflareSandboxExecResult>(() => undefined);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const promise = executeCloudflareCode(
|
|
315
|
+
{ lang: 'py', code: 'print("slow")' },
|
|
316
|
+
{ sandbox, workspaceRoot: '/workspace', timeoutMs: 1000 }
|
|
317
|
+
);
|
|
318
|
+
const assertion = expect(promise).rejects.toThrow(/client-side timeout/);
|
|
319
|
+
await jest.advanceTimersByTimeAsync(11500);
|
|
320
|
+
await assertion;
|
|
321
|
+
|
|
322
|
+
expect(mainSignal).toBeDefined();
|
|
323
|
+
expect(mainSignal?.aborted).toBe(true);
|
|
324
|
+
} finally {
|
|
325
|
+
jest.useRealTimers();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('composes a caller abort signal with the timeout instead of clobbering it', async () => {
|
|
330
|
+
let received: AbortSignal | undefined;
|
|
331
|
+
const sandbox = createRuntime({
|
|
332
|
+
supportsExecSignal: true,
|
|
333
|
+
exec: (_command, options) => {
|
|
334
|
+
received = options?.signal;
|
|
335
|
+
return new Promise<t.CloudflareSandboxExecResult>(
|
|
336
|
+
(_resolve, reject) => {
|
|
337
|
+
options?.signal?.addEventListener(
|
|
338
|
+
'abort',
|
|
339
|
+
() => reject(new Error('aborted')),
|
|
340
|
+
{ once: true }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const caller = new AbortController();
|
|
348
|
+
const settled = execWithClientTimeout(
|
|
349
|
+
sandbox,
|
|
350
|
+
'echo hi',
|
|
351
|
+
{ signal: caller.signal },
|
|
352
|
+
60000,
|
|
353
|
+
'test'
|
|
354
|
+
).catch((e) => e as Error);
|
|
355
|
+
|
|
356
|
+
await Promise.resolve();
|
|
357
|
+
expect(received).toBeDefined();
|
|
358
|
+
// The exec gets a composed signal, not the caller's directly.
|
|
359
|
+
expect(received).not.toBe(caller.signal);
|
|
360
|
+
expect(received?.aborted).toBe(false);
|
|
361
|
+
|
|
362
|
+
// A caller cancellation must reach the exec (not wait for the client timeout).
|
|
363
|
+
caller.abort();
|
|
364
|
+
await settled;
|
|
365
|
+
expect(received?.aborted).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('strips a caller signal for native runtimes that cannot consume it', async () => {
|
|
369
|
+
let received: t.CloudflareSandboxExecOptions | undefined;
|
|
370
|
+
const sandbox = createRuntime({
|
|
371
|
+
// no supportsExecSignal -> native DO, which cannot clone/consume a signal
|
|
372
|
+
exec: async (_command, options) => {
|
|
373
|
+
received = options;
|
|
374
|
+
return { exitCode: 0, stdout: 'ok', stderr: '' };
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
const caller = new AbortController();
|
|
378
|
+
|
|
379
|
+
await execWithClientTimeout(
|
|
380
|
+
sandbox,
|
|
381
|
+
'echo hi',
|
|
382
|
+
{ cwd: '/workspace', signal: caller.signal },
|
|
383
|
+
60000,
|
|
384
|
+
'test'
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(received).toBeDefined();
|
|
388
|
+
expect(received).not.toHaveProperty('signal');
|
|
389
|
+
});
|
|
390
|
+
|
|
260
391
|
it('passes call-specific timeouts to the Cloudflare spawn wrapper', async () => {
|
|
261
392
|
let execCommand = '';
|
|
262
393
|
let execTimeout: number | undefined;
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from '@/tools/BashProgrammaticToolCalling';
|
|
20
20
|
import { Constants } from '@/common';
|
|
21
21
|
import {
|
|
22
|
+
clientExecTimeoutMs,
|
|
23
|
+
execWithClientTimeout,
|
|
22
24
|
executeCloudflareCode,
|
|
23
25
|
getCloudflareWorkspaceRoot,
|
|
24
26
|
resolveCloudflareSandbox,
|
|
@@ -166,13 +168,16 @@ async function executeGeneratedCloudflareBash(
|
|
|
166
168
|
const workspaceRoot = getCloudflareWorkspaceRoot(config);
|
|
167
169
|
const shell = config.shell ?? 'bash';
|
|
168
170
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
169
|
-
const result = await
|
|
171
|
+
const result = await execWithClientTimeout(
|
|
172
|
+
sandbox,
|
|
170
173
|
withInSandboxTimeout(`${shell} -lc ${quoteShell(command)}`, timeoutMs),
|
|
171
174
|
{
|
|
172
175
|
cwd: workspaceRoot,
|
|
173
176
|
env: config.env,
|
|
174
177
|
timeout: outerTimeoutMs(timeoutMs),
|
|
175
|
-
}
|
|
178
|
+
},
|
|
179
|
+
clientExecTimeoutMs(timeoutMs),
|
|
180
|
+
'cloudflare bash programmatic exec'
|
|
176
181
|
);
|
|
177
182
|
const maxOutputChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
178
183
|
return {
|