@librechat/agents 3.1.82 → 3.1.84
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 +69 -24
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/events.cjs +2 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/main.cjs +3 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +96 -0
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +5 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -3
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +28 -2
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +107 -34
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -4
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +71 -26
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/events.mjs +2 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/cache.mjs +96 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +6 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +27 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +108 -35
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -4
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +7 -3
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +6 -2
- package/dist/types/messages/cache.d.ts +1 -0
- package/dist/types/tools/CodeExecutor.d.ts +5 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +14 -3
- package/dist/types/types/tools.d.ts +6 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +102 -30
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -4
- package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +128 -0
- package/src/agents/__tests__/AgentContext.test.ts +199 -27
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +8 -2
- package/src/events.ts +4 -1
- package/src/messages/cache.ts +143 -0
- package/src/tools/BashExecutor.ts +14 -3
- package/src/tools/BashProgrammaticToolCalling.ts +6 -3
- package/src/tools/CodeExecutor.ts +36 -2
- package/src/tools/ProgrammaticToolCalling.ts +175 -30
- package/src/tools/ToolNode.ts +3 -4
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +321 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +31 -1
- package/src/types/tools.ts +10 -0
|
@@ -186,13 +186,15 @@ export declare class AgentContext {
|
|
|
186
186
|
/**
|
|
187
187
|
* Builds instructions text for tools that are ONLY callable via programmatic code execution.
|
|
188
188
|
* These tools cannot be called directly by the LLM but are available through the
|
|
189
|
-
*
|
|
189
|
+
* configured programmatic tool.
|
|
190
190
|
*
|
|
191
191
|
* Includes:
|
|
192
192
|
* - Code_execution-only tools that are NOT deferred
|
|
193
193
|
* - Code_execution-only tools that ARE deferred but have been discovered via tool search
|
|
194
194
|
*/
|
|
195
195
|
private buildProgrammaticOnlyToolsInstructions;
|
|
196
|
+
private getProgrammaticToolInstructionTarget;
|
|
197
|
+
private hasAvailableTool;
|
|
196
198
|
/**
|
|
197
199
|
* Gets the system runnable, creating it lazily if needed.
|
|
198
200
|
* Includes stable instructions, dynamic additional instructions, and
|
|
@@ -227,8 +229,10 @@ export declare class AgentContext {
|
|
|
227
229
|
*/
|
|
228
230
|
private buildSystemRunnable;
|
|
229
231
|
private buildSummaryHumanMessage;
|
|
230
|
-
private
|
|
231
|
-
private
|
|
232
|
+
private buildPromptCacheDynamicTail;
|
|
233
|
+
private buildBodyWithPromptCacheDynamicTail;
|
|
234
|
+
private getPromptCacheDynamicTailIndex;
|
|
235
|
+
private addStablePromptCacheMarkers;
|
|
232
236
|
private getPromptCacheProvider;
|
|
233
237
|
private hasBedrockPromptCache;
|
|
234
238
|
private buildSystemMessage;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { UsageMetadata } from '@langchain/core/messages';
|
|
2
|
+
import type { ClientOptions } from '@langchain/openai';
|
|
2
3
|
import type * as t from '@/types';
|
|
3
4
|
import { Providers } from '@/common';
|
|
4
|
-
type
|
|
5
|
+
import type { ChatOpenRouterInput } from '@/llm/openrouter';
|
|
6
|
+
type LivePromptCacheProvider = Providers.ANTHROPIC | Providers.BEDROCK | Providers.OPENROUTER;
|
|
5
7
|
type PromptCacheExpectedSystemBlock = {
|
|
6
8
|
type: 'text';
|
|
7
9
|
text: string;
|
|
@@ -13,7 +15,9 @@ type PromptCacheExpectedSystemBlock = {
|
|
|
13
15
|
type: 'default';
|
|
14
16
|
};
|
|
15
17
|
};
|
|
16
|
-
type LivePromptCacheClientOptions = t.ClientOptions | t.BedrockAnthropicClientOptions
|
|
18
|
+
type LivePromptCacheClientOptions = t.ClientOptions | t.BedrockAnthropicClientOptions | (ChatOpenRouterInput & {
|
|
19
|
+
configuration?: ClientOptions;
|
|
20
|
+
});
|
|
17
21
|
export declare function buildStableInstructions({ nonce, providerLabel, }: {
|
|
18
22
|
nonce: string;
|
|
19
23
|
providerLabel: string;
|
|
@@ -13,6 +13,7 @@ type MessageWithContent = {
|
|
|
13
13
|
* @returns - A new array of message objects with cache control added.
|
|
14
14
|
*/
|
|
15
15
|
export declare function addCacheControl<T extends AnthropicMessage | BaseMessage>(messages: T[]): T[];
|
|
16
|
+
export declare function addCacheControlToStablePrefixMessages<T extends AnthropicMessage | BaseMessage>(messages: T[], maxCachePoints: number): T[];
|
|
16
17
|
/**
|
|
17
18
|
* Removes all Anthropic cache_control fields from messages
|
|
18
19
|
* Used when switching from Anthropic to Bedrock provider
|
|
@@ -25,6 +25,11 @@ export declare const CodeExecutionToolSchema: {
|
|
|
25
25
|
};
|
|
26
26
|
readonly required: readonly ["lang", "code"];
|
|
27
27
|
};
|
|
28
|
+
export declare function resolveCodeApiAuthHeaders(authHeaders?: t.CodeApiAuthHeaders): Promise<t.CodeApiAuthHeaderMap>;
|
|
29
|
+
export declare function buildCodeApiHttpErrorMessage(method: string, endpoint: string, response: {
|
|
30
|
+
status: number;
|
|
31
|
+
text: () => Promise<string>;
|
|
32
|
+
}): Promise<string>;
|
|
28
33
|
export declare const CodeExecutionToolDescription: string;
|
|
29
34
|
export declare const CodeExecutionToolName = Constants.EXECUTE_CODE;
|
|
30
35
|
export declare const CodeExecutionToolDefinition: {
|
|
@@ -43,6 +43,15 @@ export declare const ProgrammaticToolCallingDefinition: {
|
|
|
43
43
|
readonly required: readonly ["code"];
|
|
44
44
|
};
|
|
45
45
|
};
|
|
46
|
+
export type FetchSessionFilesScope = {
|
|
47
|
+
kind: 'skill';
|
|
48
|
+
id: string;
|
|
49
|
+
version: number;
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'agent' | 'user';
|
|
52
|
+
id: string;
|
|
53
|
+
version?: never;
|
|
54
|
+
};
|
|
46
55
|
/**
|
|
47
56
|
* Normalizes a tool name to Python identifier format.
|
|
48
57
|
* Must match the Code API's `normalizePythonFunctionName` exactly:
|
|
@@ -76,10 +85,12 @@ export declare function filterToolsByUsage(toolDefs: t.LCTool[], code: string, d
|
|
|
76
85
|
* Files are returned as CodeEnvFile references to be included in the request.
|
|
77
86
|
* @param baseUrl - The base URL for the Code API
|
|
78
87
|
* @param sessionId - The session ID to fetch files from
|
|
88
|
+
* @param scope - Resource scope used by CodeAPI to authorize the session
|
|
79
89
|
* @param proxy - Optional HTTP proxy URL
|
|
80
90
|
* @returns Array of CodeEnvFile references, or empty array if fetch fails
|
|
81
91
|
*/
|
|
82
|
-
export declare function fetchSessionFiles(baseUrl: string, sessionId: string, proxy?: string): Promise<t.CodeEnvFile[]>;
|
|
92
|
+
export declare function fetchSessionFiles(baseUrl: string, sessionId: string, proxy?: string, authHeaders?: t.CodeApiAuthHeaders): Promise<t.CodeEnvFile[]>;
|
|
93
|
+
export declare function fetchSessionFiles(baseUrl: string, sessionId: string, scope: FetchSessionFilesScope, proxyOrAuthHeaders?: string | t.CodeApiAuthHeaders, authHeaders?: t.CodeApiAuthHeaders): Promise<t.CodeEnvFile[]>;
|
|
83
94
|
/**
|
|
84
95
|
* Makes an HTTP request to the Code API.
|
|
85
96
|
* @param endpoint - The API endpoint URL
|
|
@@ -87,7 +98,7 @@ export declare function fetchSessionFiles(baseUrl: string, sessionId: string, pr
|
|
|
87
98
|
* @param proxy - Optional HTTP proxy URL
|
|
88
99
|
* @returns The parsed API response
|
|
89
100
|
*/
|
|
90
|
-
export declare function makeRequest(endpoint: string, body: Record<string, unknown>, proxy?: string): Promise<t.ProgrammaticExecutionResponse>;
|
|
101
|
+
export declare function makeRequest(endpoint: string, body: Record<string, unknown>, proxy?: string, authHeaders?: t.CodeApiAuthHeaders): Promise<t.ProgrammaticExecutionResponse>;
|
|
91
102
|
/**
|
|
92
103
|
* Unwraps tool responses that may be formatted as tuples or content blocks.
|
|
93
104
|
* MCP tools return [content, artifacts], we need to extract the raw data.
|
|
@@ -104,7 +115,7 @@ export declare function unwrapToolResponse(result: unknown, isMCPTool: boolean):
|
|
|
104
115
|
* @param toolMap - Map of tool names to executable tools
|
|
105
116
|
* @returns Array of tool results
|
|
106
117
|
*/
|
|
107
|
-
export declare function executeTools(toolCalls: t.PTCToolCall[], toolMap: t.ToolMap): Promise<t.PTCToolResult[]>;
|
|
118
|
+
export declare function executeTools(toolCalls: t.PTCToolCall[], toolMap: t.ToolMap, programmaticToolName?: Constants): Promise<t.PTCToolResult[]>;
|
|
108
119
|
/**
|
|
109
120
|
* Formats the completed response for the agent.
|
|
110
121
|
*
|
|
@@ -205,7 +205,11 @@ export type CodeExecutionToolParams = undefined | {
|
|
|
205
205
|
session_id?: string;
|
|
206
206
|
user_id?: string;
|
|
207
207
|
files?: CodeEnvFile[];
|
|
208
|
+
/** Optional host-supplied Code API auth headers. */
|
|
209
|
+
authHeaders?: CodeApiAuthHeaders;
|
|
208
210
|
};
|
|
211
|
+
export type CodeApiAuthHeaderMap = Record<string, string>;
|
|
212
|
+
export type CodeApiAuthHeaders = CodeApiAuthHeaderMap | (() => CodeApiAuthHeaderMap | Promise<CodeApiAuthHeaderMap>);
|
|
209
213
|
export type FileRef = {
|
|
210
214
|
/**
|
|
211
215
|
* Storage file id (the per-file uuid). See `CodeEnvFile.id` for
|
|
@@ -825,6 +829,8 @@ export type ProgrammaticToolCallingParams = {
|
|
|
825
829
|
proxy?: string;
|
|
826
830
|
/** Enable debug logging (or set PTC_DEBUG=true env var) */
|
|
827
831
|
debug?: boolean;
|
|
832
|
+
/** Optional host-supplied Code API auth headers. */
|
|
833
|
+
authHeaders?: CodeApiAuthHeaders;
|
|
828
834
|
};
|
|
829
835
|
/**
|
|
830
836
|
* Tracks code execution session state for automatic file persistence.
|
package/package.json
CHANGED
|
@@ -13,10 +13,14 @@ import {
|
|
|
13
13
|
ANTHROPIC_TOOL_TOKEN_MULTIPLIER,
|
|
14
14
|
DEFAULT_TOOL_TOKEN_MULTIPLIER,
|
|
15
15
|
ContentTypes,
|
|
16
|
+
Constants,
|
|
16
17
|
Providers,
|
|
17
18
|
} from '@/common';
|
|
18
19
|
import { createSchemaOnlyTools } from '@/tools/schema';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
addCacheControl,
|
|
22
|
+
addCacheControlToStablePrefixMessages,
|
|
23
|
+
} from '@/messages/cache';
|
|
20
24
|
import { DEFAULT_RESERVE_RATIO } from '@/messages';
|
|
21
25
|
import { toJsonSchema } from '@/utils/schema';
|
|
22
26
|
|
|
@@ -386,7 +390,7 @@ export class AgentContext {
|
|
|
386
390
|
/**
|
|
387
391
|
* Builds instructions text for tools that are ONLY callable via programmatic code execution.
|
|
388
392
|
* These tools cannot be called directly by the LLM but are available through the
|
|
389
|
-
*
|
|
393
|
+
* configured programmatic tool.
|
|
390
394
|
*
|
|
391
395
|
* Includes:
|
|
392
396
|
* - Code_execution-only tools that are NOT deferred
|
|
@@ -413,6 +417,7 @@ export class AgentContext {
|
|
|
413
417
|
|
|
414
418
|
if (programmaticOnlyTools.length === 0) return '';
|
|
415
419
|
|
|
420
|
+
const programmaticTool = this.getProgrammaticToolInstructionTarget();
|
|
416
421
|
const toolDescriptions = programmaticOnlyTools
|
|
417
422
|
.map((tool) => {
|
|
418
423
|
let desc = `- **${tool.name}**`;
|
|
@@ -428,12 +433,39 @@ export class AgentContext {
|
|
|
428
433
|
|
|
429
434
|
return (
|
|
430
435
|
'\n\n## Programmatic-Only Tools\n\n' +
|
|
431
|
-
|
|
432
|
-
|
|
436
|
+
`The following tools are available exclusively through the \`${programmaticTool.name}\` tool. ` +
|
|
437
|
+
`You cannot call these tools directly; instead, use \`${programmaticTool.name}\` with ${programmaticTool.language} code that invokes them.\n\n` +
|
|
433
438
|
toolDescriptions
|
|
434
439
|
);
|
|
435
440
|
}
|
|
436
441
|
|
|
442
|
+
private getProgrammaticToolInstructionTarget(): {
|
|
443
|
+
name: string;
|
|
444
|
+
language: 'bash' | 'Python';
|
|
445
|
+
} {
|
|
446
|
+
if (this.hasAvailableTool(Constants.BASH_PROGRAMMATIC_TOOL_CALLING)) {
|
|
447
|
+
return {
|
|
448
|
+
name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
|
|
449
|
+
language: 'bash',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (this.hasAvailableTool(Constants.PROGRAMMATIC_TOOL_CALLING)) {
|
|
454
|
+
return { name: Constants.PROGRAMMATIC_TOOL_CALLING, language: 'Python' };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING, language: 'bash' };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private hasAvailableTool(name: string): boolean {
|
|
461
|
+
if (this.toolDefinitions?.some((tool) => tool.name === name)) return true;
|
|
462
|
+
if (this.tools?.some((tool) => 'name' in tool && tool.name === name)) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
if (this.toolMap?.has(name)) return true;
|
|
466
|
+
return this.toolRegistry?.has(name) === true;
|
|
467
|
+
}
|
|
468
|
+
|
|
437
469
|
/**
|
|
438
470
|
* Gets the system runnable, creating it lazily if needed.
|
|
439
471
|
* Includes stable instructions, dynamic additional instructions, and
|
|
@@ -584,24 +616,24 @@ export class AgentContext {
|
|
|
584
616
|
}
|
|
585
617
|
|
|
586
618
|
const promptCacheProvider = this.getPromptCacheProvider();
|
|
587
|
-
const
|
|
588
|
-
promptCacheProvider
|
|
619
|
+
const shouldMoveDynamicInstructions =
|
|
620
|
+
promptCacheProvider != null &&
|
|
589
621
|
stableInstructions !== '' &&
|
|
590
622
|
dynamicInstructions !== '';
|
|
591
623
|
const systemMessage = this.buildSystemMessage({
|
|
592
624
|
stableInstructions,
|
|
593
625
|
dynamicInstructions,
|
|
594
626
|
promptCacheProvider,
|
|
627
|
+
shouldMoveDynamicInstructions,
|
|
595
628
|
});
|
|
596
629
|
|
|
597
630
|
if (this.tokenCounter) {
|
|
598
631
|
this.systemMessageTokens = systemMessage
|
|
599
632
|
? this.tokenCounter(systemMessage)
|
|
600
633
|
: 0;
|
|
601
|
-
this.dynamicInstructionTokens =
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
: 0;
|
|
634
|
+
this.dynamicInstructionTokens = shouldMoveDynamicInstructions
|
|
635
|
+
? this.tokenCounter(new HumanMessage(dynamicInstructions))
|
|
636
|
+
: 0;
|
|
605
637
|
}
|
|
606
638
|
|
|
607
639
|
return RunnableLambda.from((messages: BaseMessage[]) => {
|
|
@@ -616,16 +648,20 @@ export class AgentContext {
|
|
|
616
648
|
this.summaryText !== '';
|
|
617
649
|
|
|
618
650
|
const bodyWithSummary =
|
|
619
|
-
hasSummaryBody && promptCacheProvider
|
|
651
|
+
hasSummaryBody && promptCacheProvider == null
|
|
620
652
|
? [this.buildSummaryHumanMessage(promptCacheProvider), ...messages]
|
|
621
653
|
: messages;
|
|
622
|
-
const dynamicTail = this.
|
|
654
|
+
const dynamicTail = this.buildPromptCacheDynamicTail({
|
|
623
655
|
dynamicInstructions,
|
|
624
656
|
hasSummaryBody,
|
|
625
657
|
promptCacheProvider,
|
|
626
|
-
|
|
658
|
+
shouldMoveDynamicInstructions,
|
|
627
659
|
});
|
|
628
|
-
let body = this.
|
|
660
|
+
let body = this.buildBodyWithPromptCacheDynamicTail(
|
|
661
|
+
bodyWithSummary,
|
|
662
|
+
dynamicTail,
|
|
663
|
+
promptCacheProvider
|
|
664
|
+
);
|
|
629
665
|
|
|
630
666
|
if (
|
|
631
667
|
promptCacheProvider != null &&
|
|
@@ -662,22 +698,22 @@ export class AgentContext {
|
|
|
662
698
|
});
|
|
663
699
|
}
|
|
664
700
|
|
|
665
|
-
private
|
|
701
|
+
private buildPromptCacheDynamicTail({
|
|
666
702
|
dynamicInstructions,
|
|
667
703
|
hasSummaryBody,
|
|
668
704
|
promptCacheProvider,
|
|
669
|
-
|
|
705
|
+
shouldMoveDynamicInstructions,
|
|
670
706
|
}: {
|
|
671
707
|
dynamicInstructions: string;
|
|
672
708
|
hasSummaryBody: boolean;
|
|
673
709
|
promptCacheProvider: PromptCacheProvider | undefined;
|
|
674
|
-
|
|
710
|
+
shouldMoveDynamicInstructions: boolean;
|
|
675
711
|
}): BaseMessage[] {
|
|
676
|
-
if (promptCacheProvider
|
|
712
|
+
if (promptCacheProvider == null) {
|
|
677
713
|
return [];
|
|
678
714
|
}
|
|
679
715
|
|
|
680
|
-
const dynamicTail =
|
|
716
|
+
const dynamicTail = shouldMoveDynamicInstructions
|
|
681
717
|
? [new HumanMessage(dynamicInstructions)]
|
|
682
718
|
: [];
|
|
683
719
|
|
|
@@ -685,22 +721,59 @@ export class AgentContext {
|
|
|
685
721
|
return dynamicTail;
|
|
686
722
|
}
|
|
687
723
|
|
|
688
|
-
return [...dynamicTail, this.buildSummaryHumanMessage(
|
|
724
|
+
return [...dynamicTail, this.buildSummaryHumanMessage(undefined)];
|
|
689
725
|
}
|
|
690
726
|
|
|
691
|
-
private
|
|
727
|
+
private buildBodyWithPromptCacheDynamicTail(
|
|
692
728
|
messages: BaseMessage[],
|
|
693
|
-
tail: BaseMessage[]
|
|
729
|
+
tail: BaseMessage[],
|
|
730
|
+
promptCacheProvider: PromptCacheProvider | undefined
|
|
694
731
|
): BaseMessage[] {
|
|
695
732
|
if (tail.length === 0) {
|
|
696
733
|
return messages;
|
|
697
734
|
}
|
|
698
735
|
|
|
699
|
-
|
|
700
|
-
|
|
736
|
+
const tailIndex = this.getPromptCacheDynamicTailIndex(
|
|
737
|
+
messages,
|
|
738
|
+
promptCacheProvider
|
|
739
|
+
);
|
|
740
|
+
const stablePrefix = messages.slice(0, tailIndex);
|
|
741
|
+
const trailingMessages = messages.slice(tailIndex);
|
|
742
|
+
const cacheablePrefix = this.addStablePromptCacheMarkers(stablePrefix);
|
|
743
|
+
|
|
744
|
+
return [...cacheablePrefix, ...tail, ...trailingMessages];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private getPromptCacheDynamicTailIndex(
|
|
748
|
+
messages: BaseMessage[],
|
|
749
|
+
promptCacheProvider: PromptCacheProvider | undefined
|
|
750
|
+
): number {
|
|
751
|
+
const lastIndex = messages.length - 1;
|
|
752
|
+
|
|
753
|
+
if (lastIndex < 0) {
|
|
754
|
+
return 0;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (promptCacheProvider === Providers.OPENROUTER && messages.length === 1) {
|
|
758
|
+
return messages.length;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (messages[lastIndex].getType() === 'human') {
|
|
762
|
+
return lastIndex;
|
|
701
763
|
}
|
|
702
764
|
|
|
703
|
-
return
|
|
765
|
+
return messages.length;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private addStablePromptCacheMarkers(messages: BaseMessage[]): BaseMessage[] {
|
|
769
|
+
if (messages.length <= 1) {
|
|
770
|
+
return messages;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return [
|
|
774
|
+
messages[0],
|
|
775
|
+
...addCacheControlToStablePrefixMessages(messages.slice(1), 2),
|
|
776
|
+
];
|
|
704
777
|
}
|
|
705
778
|
|
|
706
779
|
private getPromptCacheProvider(): PromptCacheProvider | undefined {
|
|
@@ -739,10 +812,12 @@ export class AgentContext {
|
|
|
739
812
|
stableInstructions,
|
|
740
813
|
dynamicInstructions,
|
|
741
814
|
promptCacheProvider,
|
|
815
|
+
shouldMoveDynamicInstructions,
|
|
742
816
|
}: {
|
|
743
817
|
stableInstructions: string;
|
|
744
818
|
dynamicInstructions: string;
|
|
745
819
|
promptCacheProvider: PromptCacheProvider | undefined;
|
|
820
|
+
shouldMoveDynamicInstructions: boolean;
|
|
746
821
|
}): SystemMessage | undefined {
|
|
747
822
|
if (!stableInstructions && !dynamicInstructions) {
|
|
748
823
|
return undefined;
|
|
@@ -757,16 +832,13 @@ export class AgentContext {
|
|
|
757
832
|
cache_control: { type: 'ephemeral' },
|
|
758
833
|
});
|
|
759
834
|
}
|
|
760
|
-
if (dynamicInstructions) {
|
|
835
|
+
if (dynamicInstructions && !shouldMoveDynamicInstructions) {
|
|
761
836
|
content.push({ type: 'text', text: dynamicInstructions });
|
|
762
837
|
}
|
|
763
838
|
return new SystemMessage({ content } as BaseMessageFields);
|
|
764
839
|
}
|
|
765
840
|
|
|
766
|
-
if (
|
|
767
|
-
promptCacheProvider === Providers.OPENROUTER &&
|
|
768
|
-
!stableInstructions
|
|
769
|
-
) {
|
|
841
|
+
if (promptCacheProvider === Providers.OPENROUTER && !stableInstructions) {
|
|
770
842
|
return new SystemMessage(dynamicInstructions);
|
|
771
843
|
}
|
|
772
844
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/agents/__tests__/AgentContext.openrouter.live.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Live OpenRouter prompt-cache verification.
|
|
4
|
+
*
|
|
5
|
+
* Run with:
|
|
6
|
+
* RUN_OPENROUTER_PROMPT_CACHE_LIVE_TESTS=1 OPENROUTER_API_KEY=... npm test -- AgentContext.openrouter.live.test.ts --runInBand
|
|
7
|
+
*/
|
|
8
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
9
|
+
dotenvConfig({ path: process.env.DOTENV_CONFIG_PATH ?? '.env' });
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from '@jest/globals';
|
|
12
|
+
import type { ClientOptions } from '@langchain/openai';
|
|
13
|
+
import {
|
|
14
|
+
runLiveTurn,
|
|
15
|
+
assertSystemPayloadShape,
|
|
16
|
+
buildDynamicInstructions,
|
|
17
|
+
buildStableInstructions,
|
|
18
|
+
waitForCachePropagation,
|
|
19
|
+
} from './promptCacheLiveHelpers';
|
|
20
|
+
import type { ChatOpenRouterInput } from '@/llm/openrouter';
|
|
21
|
+
import { Providers } from '@/common';
|
|
22
|
+
|
|
23
|
+
const apiKey = process.env.OPENROUTER_API_KEY ?? process.env.OPENROUTER_KEY;
|
|
24
|
+
const shouldRunLive =
|
|
25
|
+
process.env.RUN_OPENROUTER_PROMPT_CACHE_LIVE_TESTS === '1' &&
|
|
26
|
+
apiKey != null &&
|
|
27
|
+
apiKey !== '';
|
|
28
|
+
|
|
29
|
+
const describeIfLive = shouldRunLive ? describe : describe.skip;
|
|
30
|
+
|
|
31
|
+
const model =
|
|
32
|
+
process.env.OPENROUTER_PROMPT_CACHE_MODEL ?? 'anthropic/claude-sonnet-4.6';
|
|
33
|
+
const providerLabel = 'OpenRouter';
|
|
34
|
+
type OpenRouterLiveClientOptions = ChatOpenRouterInput & {
|
|
35
|
+
configuration?: ClientOptions;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function createClientOptions(): OpenRouterLiveClientOptions {
|
|
39
|
+
if (apiKey == null || apiKey === '') {
|
|
40
|
+
throw new Error('OPENROUTER_API_KEY is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const reasoning = model.startsWith('google/gemini-3')
|
|
44
|
+
? { max_tokens: 16 }
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
model,
|
|
49
|
+
apiKey,
|
|
50
|
+
temperature: 0,
|
|
51
|
+
maxTokens: 256,
|
|
52
|
+
streaming: true,
|
|
53
|
+
streamUsage: true,
|
|
54
|
+
promptCache: true,
|
|
55
|
+
configuration: {
|
|
56
|
+
baseURL:
|
|
57
|
+
process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1',
|
|
58
|
+
defaultHeaders: {
|
|
59
|
+
'HTTP-Referer': 'https://librechat.ai',
|
|
60
|
+
'X-Title': 'LibreChat OpenRouter Prompt Cache Live Test',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
...(reasoning != null ? { reasoning } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describeIfLive('AgentContext OpenRouter prompt cache live API', () => {
|
|
68
|
+
it('keeps dynamic instructions outside the cached system prefix', async () => {
|
|
69
|
+
const nonce = `agent-openrouter-cache-live-${Date.now()}`;
|
|
70
|
+
const clientOptions = createClientOptions();
|
|
71
|
+
const stableInstructions = buildStableInstructions({
|
|
72
|
+
nonce,
|
|
73
|
+
providerLabel,
|
|
74
|
+
});
|
|
75
|
+
const firstDynamicInstructions = buildDynamicInstructions({
|
|
76
|
+
marker: 'alpha',
|
|
77
|
+
tailDescription:
|
|
78
|
+
'The Dynamic Marker line is runtime context and must remain outside the cached prefix.',
|
|
79
|
+
});
|
|
80
|
+
const secondDynamicInstructions = buildDynamicInstructions({
|
|
81
|
+
marker: 'bravo',
|
|
82
|
+
tailDescription:
|
|
83
|
+
'The Dynamic Marker line is runtime context and must remain outside the cached prefix.',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await assertSystemPayloadShape({
|
|
87
|
+
agentId: 'live-openrouter-cache-shape-check',
|
|
88
|
+
provider: Providers.OPENROUTER,
|
|
89
|
+
clientOptions,
|
|
90
|
+
stableInstructions,
|
|
91
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
92
|
+
expectedContent: [
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: stableInstructions,
|
|
96
|
+
cache_control: { type: 'ephemeral' },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const first = await runLiveTurn({
|
|
102
|
+
provider: Providers.OPENROUTER,
|
|
103
|
+
providerLabel,
|
|
104
|
+
clientOptions,
|
|
105
|
+
runId: `${nonce}-first`,
|
|
106
|
+
threadId: `${nonce}-thread`,
|
|
107
|
+
stableInstructions,
|
|
108
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(first.text.toLowerCase()).toContain('alpha');
|
|
112
|
+
|
|
113
|
+
await waitForCachePropagation();
|
|
114
|
+
|
|
115
|
+
const second = await runLiveTurn({
|
|
116
|
+
provider: Providers.OPENROUTER,
|
|
117
|
+
providerLabel,
|
|
118
|
+
clientOptions,
|
|
119
|
+
runId: `${nonce}-second`,
|
|
120
|
+
threadId: `${nonce}-thread`,
|
|
121
|
+
stableInstructions,
|
|
122
|
+
dynamicInstructions: secondDynamicInstructions,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(second.text.toLowerCase()).toContain('bravo');
|
|
126
|
+
expect(second.usage.input_token_details?.cache_read).toBeGreaterThan(0);
|
|
127
|
+
}, 120_000);
|
|
128
|
+
});
|