@librechat/agents 3.1.81 → 3.1.83
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 +125 -36
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +13 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +50 -13
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +17 -7
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/toolCache.cjs +55 -0
- package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -0
- package/dist/cjs/main.cjs +1 -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/ToolNode.cjs +70 -12
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +125 -36
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +13 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +50 -14
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +17 -7
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/toolCache.mjs +53 -0
- package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -0
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/cache.mjs +96 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +70 -12
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +8 -1
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +6 -2
- package/dist/types/llm/openrouter/index.d.ts +1 -0
- package/dist/types/llm/openrouter/toolCache.d.ts +2 -0
- package/dist/types/messages/cache.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +5 -0
- package/dist/types/types/run.d.ts +2 -0
- package/package.json +2 -1
- package/src/agents/AgentContext.ts +191 -40
- 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 +355 -18
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +8 -2
- package/src/graphs/Graph.ts +24 -0
- package/src/llm/custom-chat-models.smoke.test.ts +76 -0
- package/src/llm/openai/deepseek.test.ts +14 -1
- package/src/llm/openai/index.ts +38 -12
- package/src/llm/openrouter/index.ts +22 -7
- package/src/llm/openrouter/reasoning.test.ts +33 -0
- package/src/llm/openrouter/toolCache.test.ts +83 -0
- package/src/llm/openrouter/toolCache.ts +89 -0
- package/src/messages/cache.test.ts +127 -0
- package/src/messages/cache.ts +143 -0
- package/src/scripts/openrouter_prompt_cache_live.ts +310 -0
- package/src/specs/agent-handoffs.live.test.ts +140 -0
- package/src/specs/agent-handoffs.test.ts +266 -2
- package/src/specs/openrouter.simple.test.ts +15 -8
- package/src/tools/ToolNode.ts +92 -13
- package/src/types/run.ts +2 -0
|
@@ -54,6 +54,8 @@ export declare class AgentContext {
|
|
|
54
54
|
tokenCounter?: t.TokenCounter;
|
|
55
55
|
/** Token count for the system message (instructions text). */
|
|
56
56
|
systemMessageTokens: number;
|
|
57
|
+
/** Token count for instruction text emitted outside the system message. */
|
|
58
|
+
dynamicInstructionTokens: number;
|
|
57
59
|
/** Token count for tool schemas only. */
|
|
58
60
|
toolSchemaTokens: number;
|
|
59
61
|
/** Running calibration ratio from the pruner — persisted across runs via contextMeta. */
|
|
@@ -224,7 +226,12 @@ export declare class AgentContext {
|
|
|
224
226
|
* Only called when content has actually changed.
|
|
225
227
|
*/
|
|
226
228
|
private buildSystemRunnable;
|
|
227
|
-
private
|
|
229
|
+
private buildSummaryHumanMessage;
|
|
230
|
+
private buildPromptCacheDynamicTail;
|
|
231
|
+
private buildBodyWithPromptCacheDynamicTail;
|
|
232
|
+
private getPromptCacheDynamicTailIndex;
|
|
233
|
+
private addStablePromptCacheMarkers;
|
|
234
|
+
private getPromptCacheProvider;
|
|
228
235
|
private hasBedrockPromptCache;
|
|
229
236
|
private buildSystemMessage;
|
|
230
237
|
/**
|
|
@@ -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;
|
|
@@ -15,6 +15,7 @@ export interface ChatOpenRouterCallOptions extends Omit<ChatOpenAICallOptions, '
|
|
|
15
15
|
include_reasoning?: boolean;
|
|
16
16
|
reasoning?: OpenRouterReasoning;
|
|
17
17
|
modelKwargs?: OpenAIChatInput['modelKwargs'];
|
|
18
|
+
promptCache?: boolean;
|
|
18
19
|
}
|
|
19
20
|
export type ChatOpenRouterInput = Partial<ChatOpenRouterCallOptions & OpenAIChatInput>;
|
|
20
21
|
/** invocationParams return type extended with OpenRouter reasoning */
|
|
@@ -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
|
|
@@ -152,6 +152,11 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
152
152
|
* `createLocalCodingToolBundle()` use.
|
|
153
153
|
*/
|
|
154
154
|
getFileCheckpointer(): t.LocalFileCheckpointer | undefined;
|
|
155
|
+
private getRegisteredHandoffNames;
|
|
156
|
+
private hasRegisteredHandoffTool;
|
|
157
|
+
private getHandoffToolNameSuggestion;
|
|
158
|
+
private shouldHandleUnknownHandoffLocally;
|
|
159
|
+
private getUnknownToolErrorMessage;
|
|
155
160
|
/**
|
|
156
161
|
* Flush the per-Run direct-path turn cache. Called by the Graph at
|
|
157
162
|
* end-of-Run via `clearHeavyState`. The map intentionally survives
|
|
@@ -195,6 +195,8 @@ export type TokenBudgetBreakdown = {
|
|
|
195
195
|
instructionTokens: number;
|
|
196
196
|
/** Tokens from the system message text alone. */
|
|
197
197
|
systemMessageTokens: number;
|
|
198
|
+
/** Tokens from instruction text emitted outside the system message. */
|
|
199
|
+
dynamicInstructionTokens: number;
|
|
198
200
|
/** Tokens from tool schema definitions. */
|
|
199
201
|
toolSchemaTokens: number;
|
|
200
202
|
/** Tokens from the conversation summary. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.83",
|
|
4
4
|
"main": "./dist/cjs/main.cjs",
|
|
5
5
|
"module": "./dist/esm/main.mjs",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -171,6 +171,7 @@
|
|
|
171
171
|
"start:dev": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/main.ts",
|
|
172
172
|
"supervised": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/supervised.ts --provider anthropic --name Jo --location \"New York, NY\"",
|
|
173
173
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
174
|
+
"test:live:handoffs": "RUN_HANDOFF_LIVE_TESTS=1 NODE_OPTIONS='--experimental-vm-modules' jest src/specs/agent-handoffs.live.test.ts --runInBand",
|
|
174
175
|
"test:memory": "NODE_OPTIONS='--expose-gc' npx jest src/specs/title.memory-leak.test.ts",
|
|
175
176
|
"test:all": "npm test -- --testPathIgnorePatterns=title.memory-leak.test.ts && npm run test:memory",
|
|
176
177
|
"reinstall": "npm run clean && npm ci && rm -rf ./dist && npm run build",
|
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
Providers,
|
|
17
17
|
} from '@/common';
|
|
18
18
|
import { createSchemaOnlyTools } from '@/tools/schema';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
addCacheControl,
|
|
21
|
+
addCacheControlToStablePrefixMessages,
|
|
22
|
+
} from '@/messages/cache';
|
|
20
23
|
import { DEFAULT_RESERVE_RATIO } from '@/messages';
|
|
21
24
|
import { toJsonSchema } from '@/utils/schema';
|
|
22
25
|
|
|
@@ -30,6 +33,8 @@ type AgentSystemContentBlock =
|
|
|
30
33
|
| AgentSystemTextBlock
|
|
31
34
|
| { cachePoint: { type: 'default' } };
|
|
32
35
|
|
|
36
|
+
type PromptCacheProvider = Providers.ANTHROPIC | Providers.OPENROUTER;
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Encapsulates agent-specific state that can vary between agents in a multi-agent system
|
|
35
40
|
*/
|
|
@@ -177,6 +182,8 @@ export class AgentContext {
|
|
|
177
182
|
tokenCounter?: t.TokenCounter;
|
|
178
183
|
/** Token count for the system message (instructions text). */
|
|
179
184
|
systemMessageTokens: number = 0;
|
|
185
|
+
/** Token count for instruction text emitted outside the system message. */
|
|
186
|
+
dynamicInstructionTokens: number = 0;
|
|
180
187
|
/** Token count for tool schemas only. */
|
|
181
188
|
toolSchemaTokens: number = 0;
|
|
182
189
|
/** Running calibration ratio from the pruner — persisted across runs via contextMeta. */
|
|
@@ -190,7 +197,12 @@ export class AgentContext {
|
|
|
190
197
|
get instructionTokens(): number {
|
|
191
198
|
const summaryOverhead =
|
|
192
199
|
this._summaryLocation === 'user_message' ? this.summaryTokenCount : 0;
|
|
193
|
-
return
|
|
200
|
+
return (
|
|
201
|
+
this.systemMessageTokens +
|
|
202
|
+
this.dynamicInstructionTokens +
|
|
203
|
+
this.toolSchemaTokens +
|
|
204
|
+
summaryOverhead
|
|
205
|
+
);
|
|
194
206
|
}
|
|
195
207
|
/** The amount of time that should pass before another consecutive API call */
|
|
196
208
|
streamBuffer?: number;
|
|
@@ -570,20 +582,29 @@ export class AgentContext {
|
|
|
570
582
|
|
|
571
583
|
if (!stableInstructions && !dynamicInstructions && !hasMidRunSummary) {
|
|
572
584
|
this.systemMessageTokens = 0;
|
|
585
|
+
this.dynamicInstructionTokens = 0;
|
|
573
586
|
return undefined;
|
|
574
587
|
}
|
|
575
588
|
|
|
576
|
-
const
|
|
589
|
+
const promptCacheProvider = this.getPromptCacheProvider();
|
|
590
|
+
const shouldMoveDynamicInstructions =
|
|
591
|
+
promptCacheProvider != null &&
|
|
592
|
+
stableInstructions !== '' &&
|
|
593
|
+
dynamicInstructions !== '';
|
|
577
594
|
const systemMessage = this.buildSystemMessage({
|
|
578
595
|
stableInstructions,
|
|
579
596
|
dynamicInstructions,
|
|
580
|
-
|
|
597
|
+
promptCacheProvider,
|
|
598
|
+
shouldMoveDynamicInstructions,
|
|
581
599
|
});
|
|
582
600
|
|
|
583
601
|
if (this.tokenCounter) {
|
|
584
602
|
this.systemMessageTokens = systemMessage
|
|
585
603
|
? this.tokenCounter(systemMessage)
|
|
586
604
|
: 0;
|
|
605
|
+
this.dynamicInstructionTokens = shouldMoveDynamicInstructions
|
|
606
|
+
? this.tokenCounter(new HumanMessage(dynamicInstructions))
|
|
607
|
+
: 0;
|
|
587
608
|
}
|
|
588
609
|
|
|
589
610
|
return RunnableLambda.from((messages: BaseMessage[]) => {
|
|
@@ -597,45 +618,155 @@ export class AgentContext {
|
|
|
597
618
|
this.summaryText != null &&
|
|
598
619
|
this.summaryText !== '';
|
|
599
620
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
},
|
|
616
|
-
],
|
|
617
|
-
})
|
|
618
|
-
: new HumanMessage(wrappedSummary);
|
|
619
|
-
body = [summaryMsg, ...messages];
|
|
620
|
-
} else {
|
|
621
|
-
body = messages;
|
|
622
|
-
}
|
|
621
|
+
const bodyWithSummary =
|
|
622
|
+
hasSummaryBody && promptCacheProvider == null
|
|
623
|
+
? [this.buildSummaryHumanMessage(promptCacheProvider), ...messages]
|
|
624
|
+
: messages;
|
|
625
|
+
const dynamicTail = this.buildPromptCacheDynamicTail({
|
|
626
|
+
dynamicInstructions,
|
|
627
|
+
hasSummaryBody,
|
|
628
|
+
promptCacheProvider,
|
|
629
|
+
shouldMoveDynamicInstructions,
|
|
630
|
+
});
|
|
631
|
+
let body = this.buildBodyWithPromptCacheDynamicTail(
|
|
632
|
+
bodyWithSummary,
|
|
633
|
+
dynamicTail,
|
|
634
|
+
promptCacheProvider
|
|
635
|
+
);
|
|
623
636
|
|
|
624
|
-
if (
|
|
637
|
+
if (
|
|
638
|
+
promptCacheProvider != null &&
|
|
639
|
+
dynamicTail.length === 0 &&
|
|
640
|
+
body.length >= 2
|
|
641
|
+
) {
|
|
625
642
|
body = addCacheControl(body);
|
|
626
643
|
}
|
|
627
644
|
return [...prefix, ...body];
|
|
628
645
|
}).withConfig({ runName: 'prompt' });
|
|
629
646
|
}
|
|
630
647
|
|
|
631
|
-
private
|
|
632
|
-
|
|
633
|
-
|
|
648
|
+
private buildSummaryHumanMessage(
|
|
649
|
+
promptCacheProvider: PromptCacheProvider | undefined
|
|
650
|
+
): HumanMessage {
|
|
651
|
+
const wrappedSummary =
|
|
652
|
+
'<summary>\n' +
|
|
653
|
+
(this.summaryText as string) +
|
|
654
|
+
'\n</summary>\n\n' +
|
|
655
|
+
'This is your own checkpoint: you wrote it to preserve context after compaction. Pick up where you left off based on the summary above. Do not repeat prior tasks, information or acknowledge this checkpoint message directly.';
|
|
656
|
+
|
|
657
|
+
if (promptCacheProvider !== Providers.ANTHROPIC) {
|
|
658
|
+
return new HumanMessage(wrappedSummary);
|
|
634
659
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
660
|
+
|
|
661
|
+
return new HumanMessage({
|
|
662
|
+
content: [
|
|
663
|
+
{
|
|
664
|
+
type: 'text',
|
|
665
|
+
text: wrappedSummary,
|
|
666
|
+
cache_control: { type: 'ephemeral' },
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private buildPromptCacheDynamicTail({
|
|
673
|
+
dynamicInstructions,
|
|
674
|
+
hasSummaryBody,
|
|
675
|
+
promptCacheProvider,
|
|
676
|
+
shouldMoveDynamicInstructions,
|
|
677
|
+
}: {
|
|
678
|
+
dynamicInstructions: string;
|
|
679
|
+
hasSummaryBody: boolean;
|
|
680
|
+
promptCacheProvider: PromptCacheProvider | undefined;
|
|
681
|
+
shouldMoveDynamicInstructions: boolean;
|
|
682
|
+
}): BaseMessage[] {
|
|
683
|
+
if (promptCacheProvider == null) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const dynamicTail = shouldMoveDynamicInstructions
|
|
688
|
+
? [new HumanMessage(dynamicInstructions)]
|
|
689
|
+
: [];
|
|
690
|
+
|
|
691
|
+
if (!hasSummaryBody) {
|
|
692
|
+
return dynamicTail;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return [...dynamicTail, this.buildSummaryHumanMessage(undefined)];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private buildBodyWithPromptCacheDynamicTail(
|
|
699
|
+
messages: BaseMessage[],
|
|
700
|
+
tail: BaseMessage[],
|
|
701
|
+
promptCacheProvider: PromptCacheProvider | undefined
|
|
702
|
+
): BaseMessage[] {
|
|
703
|
+
if (tail.length === 0) {
|
|
704
|
+
return messages;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const tailIndex = this.getPromptCacheDynamicTailIndex(
|
|
708
|
+
messages,
|
|
709
|
+
promptCacheProvider
|
|
710
|
+
);
|
|
711
|
+
const stablePrefix = messages.slice(0, tailIndex);
|
|
712
|
+
const trailingMessages = messages.slice(tailIndex);
|
|
713
|
+
const cacheablePrefix = this.addStablePromptCacheMarkers(stablePrefix);
|
|
714
|
+
|
|
715
|
+
return [...cacheablePrefix, ...tail, ...trailingMessages];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private getPromptCacheDynamicTailIndex(
|
|
719
|
+
messages: BaseMessage[],
|
|
720
|
+
promptCacheProvider: PromptCacheProvider | undefined
|
|
721
|
+
): number {
|
|
722
|
+
const lastIndex = messages.length - 1;
|
|
723
|
+
|
|
724
|
+
if (lastIndex < 0) {
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (promptCacheProvider === Providers.OPENROUTER && messages.length === 1) {
|
|
729
|
+
return messages.length;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (messages[lastIndex].getType() === 'human') {
|
|
733
|
+
return lastIndex;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return messages.length;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private addStablePromptCacheMarkers(messages: BaseMessage[]): BaseMessage[] {
|
|
740
|
+
if (messages.length <= 1) {
|
|
741
|
+
return messages;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return [
|
|
745
|
+
messages[0],
|
|
746
|
+
...addCacheControlToStablePrefixMessages(messages.slice(1), 2),
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private getPromptCacheProvider(): PromptCacheProvider | undefined {
|
|
751
|
+
if (this.provider === Providers.ANTHROPIC) {
|
|
752
|
+
const anthropicOptions = this.clientOptions as
|
|
753
|
+
| t.AnthropicClientOptions
|
|
754
|
+
| undefined;
|
|
755
|
+
return anthropicOptions?.promptCache === true
|
|
756
|
+
? Providers.ANTHROPIC
|
|
757
|
+
: undefined;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (this.provider === Providers.OPENROUTER) {
|
|
761
|
+
const openRouterOptions = this.clientOptions as
|
|
762
|
+
| t.ProviderOptionsMap[Providers.OPENROUTER]
|
|
763
|
+
| undefined;
|
|
764
|
+
return openRouterOptions?.promptCache === true
|
|
765
|
+
? Providers.OPENROUTER
|
|
766
|
+
: undefined;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return undefined;
|
|
639
770
|
}
|
|
640
771
|
|
|
641
772
|
private hasBedrockPromptCache(): boolean {
|
|
@@ -651,17 +782,19 @@ export class AgentContext {
|
|
|
651
782
|
private buildSystemMessage({
|
|
652
783
|
stableInstructions,
|
|
653
784
|
dynamicInstructions,
|
|
654
|
-
|
|
785
|
+
promptCacheProvider,
|
|
786
|
+
shouldMoveDynamicInstructions,
|
|
655
787
|
}: {
|
|
656
788
|
stableInstructions: string;
|
|
657
789
|
dynamicInstructions: string;
|
|
658
|
-
|
|
790
|
+
promptCacheProvider: PromptCacheProvider | undefined;
|
|
791
|
+
shouldMoveDynamicInstructions: boolean;
|
|
659
792
|
}): SystemMessage | undefined {
|
|
660
793
|
if (!stableInstructions && !dynamicInstructions) {
|
|
661
794
|
return undefined;
|
|
662
795
|
}
|
|
663
796
|
|
|
664
|
-
if (
|
|
797
|
+
if (promptCacheProvider === Providers.ANTHROPIC) {
|
|
665
798
|
const content: AgentSystemContentBlock[] = [];
|
|
666
799
|
if (stableInstructions) {
|
|
667
800
|
content.push({
|
|
@@ -670,12 +803,28 @@ export class AgentContext {
|
|
|
670
803
|
cache_control: { type: 'ephemeral' },
|
|
671
804
|
});
|
|
672
805
|
}
|
|
673
|
-
if (dynamicInstructions) {
|
|
806
|
+
if (dynamicInstructions && !shouldMoveDynamicInstructions) {
|
|
674
807
|
content.push({ type: 'text', text: dynamicInstructions });
|
|
675
808
|
}
|
|
676
809
|
return new SystemMessage({ content } as BaseMessageFields);
|
|
677
810
|
}
|
|
678
811
|
|
|
812
|
+
if (promptCacheProvider === Providers.OPENROUTER && !stableInstructions) {
|
|
813
|
+
return new SystemMessage(dynamicInstructions);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (promptCacheProvider === Providers.OPENROUTER) {
|
|
817
|
+
return new SystemMessage({
|
|
818
|
+
content: [
|
|
819
|
+
{
|
|
820
|
+
type: 'text',
|
|
821
|
+
text: stableInstructions,
|
|
822
|
+
cache_control: { type: 'ephemeral' },
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
} as BaseMessageFields);
|
|
826
|
+
}
|
|
827
|
+
|
|
679
828
|
if (this.hasBedrockPromptCache() && stableInstructions) {
|
|
680
829
|
const content: AgentSystemContentBlock[] = [
|
|
681
830
|
{ type: 'text', text: stableInstructions },
|
|
@@ -699,6 +848,7 @@ export class AgentContext {
|
|
|
699
848
|
*/
|
|
700
849
|
reset(): void {
|
|
701
850
|
this.systemMessageTokens = 0;
|
|
851
|
+
this.dynamicInstructionTokens = 0;
|
|
702
852
|
this.toolSchemaTokens = 0;
|
|
703
853
|
this.cachedSystemRunnable = undefined;
|
|
704
854
|
this.systemRunnableStale = true;
|
|
@@ -1054,6 +1204,7 @@ export class AgentContext {
|
|
|
1054
1204
|
maxContextTokens,
|
|
1055
1205
|
instructionTokens: this.instructionTokens,
|
|
1056
1206
|
systemMessageTokens: this.systemMessageTokens,
|
|
1207
|
+
dynamicInstructionTokens: this.dynamicInstructionTokens,
|
|
1057
1208
|
toolSchemaTokens: this.toolSchemaTokens,
|
|
1058
1209
|
summaryTokens: this.summaryTokenCount,
|
|
1059
1210
|
toolCount,
|
|
@@ -1072,7 +1223,7 @@ export class AgentContext {
|
|
|
1072
1223
|
const lines = [
|
|
1073
1224
|
'Token budget breakdown:',
|
|
1074
1225
|
` maxContextTokens: ${b.maxContextTokens}`,
|
|
1075
|
-
` instructionTokens: ${b.instructionTokens} (system: ${b.systemMessageTokens}, tools: ${b.toolSchemaTokens} [${b.toolCount} tools])`,
|
|
1226
|
+
` instructionTokens: ${b.instructionTokens} (system: ${b.systemMessageTokens}, dynamic: ${b.dynamicInstructionTokens}, tools: ${b.toolSchemaTokens} [${b.toolCount} tools])`,
|
|
1076
1227
|
` summaryTokens: ${b.summaryTokens}`,
|
|
1077
1228
|
` messageTokens: ${b.messageTokens} (${b.messageCount} messages)`,
|
|
1078
1229
|
` availableForMessages: ${b.availableForMessages}`,
|
|
@@ -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
|
+
});
|