@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.
Files changed (57) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +69 -24
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/events.cjs +2 -1
  4. package/dist/cjs/events.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +3 -0
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/messages/cache.cjs +96 -0
  8. package/dist/cjs/messages/cache.cjs.map +1 -1
  9. package/dist/cjs/tools/BashExecutor.cjs +5 -2
  10. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  11. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -3
  12. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  13. package/dist/cjs/tools/CodeExecutor.cjs +28 -2
  14. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  15. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +107 -34
  16. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  17. package/dist/cjs/tools/ToolNode.cjs +3 -4
  18. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  19. package/dist/esm/agents/AgentContext.mjs +71 -26
  20. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  21. package/dist/esm/events.mjs +2 -1
  22. package/dist/esm/events.mjs.map +1 -1
  23. package/dist/esm/main.mjs +2 -2
  24. package/dist/esm/messages/cache.mjs +96 -1
  25. package/dist/esm/messages/cache.mjs.map +1 -1
  26. package/dist/esm/tools/BashExecutor.mjs +6 -3
  27. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  28. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  29. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  30. package/dist/esm/tools/CodeExecutor.mjs +27 -3
  31. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  32. package/dist/esm/tools/ProgrammaticToolCalling.mjs +108 -35
  33. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  34. package/dist/esm/tools/ToolNode.mjs +3 -4
  35. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  36. package/dist/types/agents/AgentContext.d.ts +7 -3
  37. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +6 -2
  38. package/dist/types/messages/cache.d.ts +1 -0
  39. package/dist/types/tools/CodeExecutor.d.ts +5 -0
  40. package/dist/types/tools/ProgrammaticToolCalling.d.ts +14 -3
  41. package/dist/types/types/tools.d.ts +6 -0
  42. package/package.json +1 -1
  43. package/src/agents/AgentContext.ts +102 -30
  44. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -4
  45. package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +128 -0
  46. package/src/agents/__tests__/AgentContext.test.ts +199 -27
  47. package/src/agents/__tests__/promptCacheLiveHelpers.ts +8 -2
  48. package/src/events.ts +4 -1
  49. package/src/messages/cache.ts +143 -0
  50. package/src/tools/BashExecutor.ts +14 -3
  51. package/src/tools/BashProgrammaticToolCalling.ts +6 -3
  52. package/src/tools/CodeExecutor.ts +36 -2
  53. package/src/tools/ProgrammaticToolCalling.ts +175 -30
  54. package/src/tools/ToolNode.ts +3 -4
  55. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +321 -0
  56. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +31 -1
  57. 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
- * run_tools_with_code tool.
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 buildOpenRouterDynamicTail;
231
- private insertAfterFirstMessage;
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 LivePromptCacheProvider = Providers.ANTHROPIC | Providers.BEDROCK;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.1.82",
3
+ "version": "3.1.84",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -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 { addCacheControl } from '@/messages/cache';
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
- * run_tools_with_code tool.
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
- 'The following tools are available exclusively through the `run_tools_with_code` tool. ' +
432
- 'You cannot call these tools directly; instead, use `run_tools_with_code` with Python code that invokes them.\n\n' +
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 shouldMoveOpenRouterDynamicInstructions =
588
- promptCacheProvider === Providers.OPENROUTER &&
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
- shouldMoveOpenRouterDynamicInstructions
603
- ? this.tokenCounter(new HumanMessage(dynamicInstructions))
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 !== Providers.OPENROUTER
651
+ hasSummaryBody && promptCacheProvider == null
620
652
  ? [this.buildSummaryHumanMessage(promptCacheProvider), ...messages]
621
653
  : messages;
622
- const dynamicTail = this.buildOpenRouterDynamicTail({
654
+ const dynamicTail = this.buildPromptCacheDynamicTail({
623
655
  dynamicInstructions,
624
656
  hasSummaryBody,
625
657
  promptCacheProvider,
626
- shouldMoveOpenRouterDynamicInstructions,
658
+ shouldMoveDynamicInstructions,
627
659
  });
628
- let body = this.insertAfterFirstMessage(bodyWithSummary, dynamicTail);
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 buildOpenRouterDynamicTail({
701
+ private buildPromptCacheDynamicTail({
666
702
  dynamicInstructions,
667
703
  hasSummaryBody,
668
704
  promptCacheProvider,
669
- shouldMoveOpenRouterDynamicInstructions,
705
+ shouldMoveDynamicInstructions,
670
706
  }: {
671
707
  dynamicInstructions: string;
672
708
  hasSummaryBody: boolean;
673
709
  promptCacheProvider: PromptCacheProvider | undefined;
674
- shouldMoveOpenRouterDynamicInstructions: boolean;
710
+ shouldMoveDynamicInstructions: boolean;
675
711
  }): BaseMessage[] {
676
- if (promptCacheProvider !== Providers.OPENROUTER) {
712
+ if (promptCacheProvider == null) {
677
713
  return [];
678
714
  }
679
715
 
680
- const dynamicTail = shouldMoveOpenRouterDynamicInstructions
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(promptCacheProvider)];
724
+ return [...dynamicTail, this.buildSummaryHumanMessage(undefined)];
689
725
  }
690
726
 
691
- private insertAfterFirstMessage(
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
- if (messages.length === 0) {
700
- return tail;
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 [messages[0], ...tail, ...messages.slice(1)];
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
 
@@ -77,10 +77,6 @@ describeIfLive('AgentContext Anthropic prompt cache live API', () => {
77
77
  text: stableInstructions,
78
78
  cache_control: { type: 'ephemeral' },
79
79
  },
80
- {
81
- type: 'text',
82
- text: firstDynamicInstructions,
83
- },
84
80
  ],
85
81
  });
86
82
 
@@ -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
+ });