@librechat/agents 3.2.34 → 3.2.36

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 (128) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +119 -9
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/agents/projection.cjs +25 -0
  4. package/dist/cjs/agents/projection.cjs.map +1 -0
  5. package/dist/cjs/common/enum.cjs +13 -0
  6. package/dist/cjs/common/enum.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +106 -3
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +26 -4
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +20 -0
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +49 -8
  14. package/dist/cjs/llm/invoke.cjs.map +1 -1
  15. package/dist/cjs/main.cjs +7 -0
  16. package/dist/cjs/messages/budget.cjs +23 -0
  17. package/dist/cjs/messages/budget.cjs.map +1 -0
  18. package/dist/cjs/messages/cache.cjs +1 -0
  19. package/dist/cjs/messages/cache.cjs.map +1 -1
  20. package/dist/cjs/messages/content.cjs +12 -14
  21. package/dist/cjs/messages/content.cjs.map +1 -1
  22. package/dist/cjs/messages/index.cjs +1 -0
  23. package/dist/cjs/messages/prune.cjs +31 -13
  24. package/dist/cjs/messages/prune.cjs.map +1 -1
  25. package/dist/cjs/run.cjs +7 -2
  26. package/dist/cjs/run.cjs.map +1 -1
  27. package/dist/cjs/summarization/node.cjs +12 -1
  28. package/dist/cjs/summarization/node.cjs.map +1 -1
  29. package/dist/cjs/tools/search/format.cjs +91 -2
  30. package/dist/cjs/tools/search/format.cjs.map +1 -1
  31. package/dist/cjs/tools/search/tool.cjs +4 -3
  32. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  33. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +138 -2
  34. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  35. package/dist/cjs/utils/tokens.cjs +30 -0
  36. package/dist/cjs/utils/tokens.cjs.map +1 -1
  37. package/dist/esm/agents/AgentContext.mjs +121 -11
  38. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  39. package/dist/esm/agents/projection.mjs +25 -0
  40. package/dist/esm/agents/projection.mjs.map +1 -0
  41. package/dist/esm/common/enum.mjs +13 -0
  42. package/dist/esm/common/enum.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +107 -4
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +26 -4
  46. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  47. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +20 -0
  48. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  49. package/dist/esm/llm/invoke.mjs +49 -8
  50. package/dist/esm/llm/invoke.mjs.map +1 -1
  51. package/dist/esm/main.mjs +6 -4
  52. package/dist/esm/messages/budget.mjs +23 -0
  53. package/dist/esm/messages/budget.mjs.map +1 -0
  54. package/dist/esm/messages/cache.mjs +1 -1
  55. package/dist/esm/messages/cache.mjs.map +1 -1
  56. package/dist/esm/messages/content.mjs +12 -15
  57. package/dist/esm/messages/content.mjs.map +1 -1
  58. package/dist/esm/messages/index.mjs +1 -0
  59. package/dist/esm/messages/prune.mjs +31 -13
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/run.mjs +7 -2
  62. package/dist/esm/run.mjs.map +1 -1
  63. package/dist/esm/summarization/node.mjs +12 -1
  64. package/dist/esm/summarization/node.mjs.map +1 -1
  65. package/dist/esm/tools/search/format.mjs +91 -2
  66. package/dist/esm/tools/search/format.mjs.map +1 -1
  67. package/dist/esm/tools/search/tool.mjs +4 -3
  68. package/dist/esm/tools/search/tool.mjs.map +1 -1
  69. package/dist/esm/tools/subagent/SubagentExecutor.mjs +138 -2
  70. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  71. package/dist/esm/utils/tokens.mjs +30 -1
  72. package/dist/esm/utils/tokens.mjs.map +1 -1
  73. package/dist/types/agents/AgentContext.d.ts +37 -4
  74. package/dist/types/agents/projection.d.ts +26 -0
  75. package/dist/types/common/enum.d.ts +13 -0
  76. package/dist/types/graphs/Graph.d.ts +8 -1
  77. package/dist/types/index.d.ts +1 -0
  78. package/dist/types/llm/invoke.d.ts +1 -1
  79. package/dist/types/messages/budget.d.ts +11 -0
  80. package/dist/types/messages/cache.d.ts +7 -0
  81. package/dist/types/messages/content.d.ts +5 -0
  82. package/dist/types/messages/index.d.ts +1 -0
  83. package/dist/types/messages/prune.d.ts +4 -0
  84. package/dist/types/run.d.ts +1 -0
  85. package/dist/types/tools/search/format.d.ts +4 -1
  86. package/dist/types/tools/search/types.d.ts +7 -0
  87. package/dist/types/tools/subagent/SubagentExecutor.d.ts +11 -1
  88. package/dist/types/types/graph.d.ts +89 -3
  89. package/dist/types/types/run.d.ts +13 -0
  90. package/dist/types/utils/tokens.d.ts +7 -0
  91. package/package.json +1 -1
  92. package/src/agents/AgentContext.ts +172 -8
  93. package/src/agents/__tests__/AgentContext.test.ts +235 -2
  94. package/src/agents/__tests__/projection.test.ts +73 -0
  95. package/src/agents/projection.ts +46 -0
  96. package/src/common/enum.ts +13 -0
  97. package/src/graphs/Graph.ts +168 -0
  98. package/src/index.ts +3 -0
  99. package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +317 -0
  100. package/src/llm/anthropic/utils/message_inputs.ts +78 -16
  101. package/src/llm/bedrock/utils/cross-provider-reasoning.test.ts +131 -0
  102. package/src/llm/bedrock/utils/message_inputs.ts +35 -0
  103. package/src/llm/invoke.test.ts +79 -1
  104. package/src/llm/invoke.ts +58 -4
  105. package/src/messages/budget.ts +32 -0
  106. package/src/messages/cache.ts +1 -1
  107. package/src/messages/content.ts +24 -32
  108. package/src/messages/index.ts +1 -0
  109. package/src/messages/prune.ts +39 -2
  110. package/src/run.ts +5 -0
  111. package/src/scripts/subagent-usage-sink.ts +176 -0
  112. package/src/specs/context-accuracy.live.test.ts +409 -0
  113. package/src/specs/context-usage-event.test.ts +117 -0
  114. package/src/specs/context-usage.live.test.ts +297 -0
  115. package/src/specs/prune.test.ts +51 -1
  116. package/src/specs/subagent.test.ts +124 -1
  117. package/src/summarization/__tests__/node.test.ts +60 -1
  118. package/src/summarization/node.ts +20 -1
  119. package/src/tools/__tests__/SubagentExecutor.test.ts +443 -1
  120. package/src/tools/search/format.test.ts +242 -0
  121. package/src/tools/search/format.ts +122 -5
  122. package/src/tools/search/tool.ts +5 -1
  123. package/src/tools/search/types.ts +7 -0
  124. package/src/tools/subagent/SubagentExecutor.ts +221 -3
  125. package/src/types/graph.ts +94 -1
  126. package/src/types/run.ts +13 -0
  127. package/src/utils/__tests__/apportion.test.ts +32 -0
  128. package/src/utils/tokens.ts +33 -0
@@ -23,7 +23,9 @@ import {
23
23
  formatArtifactPayload,
24
24
  enforceOriginalContentCap,
25
25
  formatContentStrings,
26
+ isLegacyConvertible,
26
27
  createPruneMessages,
28
+ syncBudgetDerivedFields,
27
29
  addCacheControl,
28
30
  getMessageId,
29
31
  makeIsDeferred,
@@ -45,6 +47,7 @@ import {
45
47
  isAnthropicLike,
46
48
  isOpenAILike,
47
49
  isGoogleLike,
50
+ apportionTokenCounts,
48
51
  joinKeys,
49
52
  sleep,
50
53
  } from '@/utils';
@@ -89,6 +92,26 @@ const { AGENT, TOOLS, SUMMARIZE } = GraphNodeKeys;
89
92
  /** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
90
93
  const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
91
94
 
95
+ /**
96
+ * Start index of the span post-prune formatters can mutate in place: the
97
+ * trailing tool batch plus its owning AI message (artifact formatting touches
98
+ * every tool result after the last AI tool call; Bedrock rewrites the AI
99
+ * message before a trailing tool result). Capped so the usage-snapshot
100
+ * recount stays constant-cost.
101
+ */
102
+ function trailingMutationStart(messages: BaseMessage[]): number {
103
+ const MAX_SPAN = 16;
104
+ let index = messages.length - 1;
105
+ while (
106
+ index >= 0 &&
107
+ messages[index]?.getType() === 'tool' &&
108
+ messages.length - index < MAX_SPAN
109
+ ) {
110
+ index--;
111
+ }
112
+ return Math.max(0, Math.min(index, messages.length - 2));
113
+ }
114
+
92
115
  type ReasoningKey = 'reasoning_content' | 'reasoning';
93
116
  type ReasoningSummary = { summary?: Array<{ text?: string }> };
94
117
  type ReasoningDetail = { type?: string; text?: string };
@@ -825,6 +848,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
825
848
  agentContexts: Map<string, AgentContext> = new Map();
826
849
  /** Default agent ID to use */
827
850
  defaultAgentId: string;
851
+ /**
852
+ * Host sink for model usage emitted inside subagent child runs. Threaded
853
+ * into each `SubagentExecutor` this graph creates (and from there into
854
+ * child graphs, so nested subagents report too). See
855
+ * {@link t.StandardGraphInput.subagentUsageSink}.
856
+ */
857
+ subagentUsageSink?: t.SubagentUsageSink;
828
858
 
829
859
  constructor({
830
860
  runId,
@@ -834,11 +864,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
834
864
  tokenCounter,
835
865
  indexTokenCountMap,
836
866
  calibrationRatio,
867
+ subagentUsageSink,
837
868
  }: t.StandardGraphInput) {
838
869
  super();
839
870
  this.runId = runId;
840
871
  this.signal = signal;
841
872
  this.langfuse = langfuse;
873
+ this.subagentUsageSink = subagentUsageSink;
842
874
 
843
875
  if (agents.length === 0) {
844
876
  throw new Error('At least one agent configuration is required');
@@ -1423,6 +1455,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1423
1455
  this.config = config;
1424
1456
 
1425
1457
  let messagesToUse = messages;
1458
+ let contextUsage: t.ContextUsageEvent | null = null;
1426
1459
  if (
1427
1460
  !agentContext.pruneMessages &&
1428
1461
  agentContext.tokenCounter &&
@@ -1462,6 +1495,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1462
1495
  originalToolContent,
1463
1496
  calibrationRatio,
1464
1497
  resolvedInstructionOverhead,
1498
+ contextBudget,
1499
+ effectiveInstructionTokens,
1465
1500
  } = agentContext.pruneMessages({
1466
1501
  messages,
1467
1502
  usageMetadata: agentContext.currentUsage,
@@ -1489,10 +1524,42 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1489
1524
  : 1;
1490
1525
  if (variance > CALIBRATION_VARIANCE_THRESHOLD) {
1491
1526
  agentContext.toolSchemaTokens = calibratedToolTokens;
1527
+ /** Largest-remainder apportionment keeps the per-tool breakdown
1528
+ * summing exactly to the calibrated aggregate */
1529
+ if (agentContext.toolTokenCounts != null && currentToolTokens > 0) {
1530
+ agentContext.toolTokenCounts = apportionTokenCounts(
1531
+ agentContext.toolTokenCounts,
1532
+ calibratedToolTokens / currentToolTokens,
1533
+ calibratedToolTokens
1534
+ );
1535
+ }
1492
1536
  }
1493
1537
  }
1494
1538
  messagesToUse = context;
1495
1539
 
1540
+ /** Dispatched right before the model invoke — a summarization
1541
+ * detour returns from this node without an LLM call, and the
1542
+ * post-summary retry produces its own snapshot.
1543
+ *
1544
+ * The breakdown describes the post-prune prompt: counts from the
1545
+ * kept context, message tokens derived from the same calibrated
1546
+ * budget math as `remainingContextTokens` (the index map is keyed
1547
+ * by pre-prune state indices, so summing it over `context` would
1548
+ * missum); `prePruneContextTokens` carries the pre-prune metric. */
1549
+ const usageBreakdown = agentContext.getTokenBudgetBreakdown(messages);
1550
+ usageBreakdown.messageCount = context.length;
1551
+ contextUsage = {
1552
+ runId: this.runId,
1553
+ agentId,
1554
+ breakdown: usageBreakdown,
1555
+ contextBudget,
1556
+ effectiveInstructionTokens,
1557
+ prePruneContextTokens,
1558
+ remainingContextTokens,
1559
+ calibrationRatio: agentContext.calibrationRatio,
1560
+ };
1561
+ syncBudgetDerivedFields(contextUsage);
1562
+
1496
1563
  const hasPrunedMessages =
1497
1564
  agentContext.summarizationEnabled === true &&
1498
1565
  Array.isArray(messagesToRefine) &&
@@ -1598,6 +1665,33 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1598
1665
  }
1599
1666
 
1600
1667
  let finalMessages = messagesToUse;
1668
+ /** Tail snapshot for the dispatch-time usage delta: in-place
1669
+ * formatters (artifact appends, Bedrock content rewrites, legacy
1670
+ * string conversion) mutate without changing length or identity —
1671
+ * capture before they run. Legacy string conversion can also touch
1672
+ * messages before the tail, so those convertible indices are
1673
+ * tracked separately (none exist in the common case). */
1674
+ const tailStart = trailingMutationStart(messagesToUse);
1675
+ let preFormatTailTokens: number | null = null;
1676
+ let legacyIndices: number[] | null = null;
1677
+ let preFormatLegacyTokens = 0;
1678
+ if (contextUsage != null && agentContext.tokenCounter != null) {
1679
+ preFormatTailTokens = 0;
1680
+ for (const message of messagesToUse.slice(tailStart)) {
1681
+ preFormatTailTokens += agentContext.tokenCounter(message);
1682
+ }
1683
+ if (agentContext.useLegacyContent) {
1684
+ legacyIndices = [];
1685
+ for (let i = 0; i < tailStart; i++) {
1686
+ if (isLegacyConvertible(messagesToUse[i])) {
1687
+ legacyIndices.push(i);
1688
+ preFormatLegacyTokens += agentContext.tokenCounter(
1689
+ messagesToUse[i]
1690
+ );
1691
+ }
1692
+ }
1693
+ }
1694
+ }
1601
1695
  if (agentContext.useLegacyContent) {
1602
1696
  finalMessages = formatContentStrings(finalMessages);
1603
1697
  }
@@ -1788,6 +1882,79 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1788
1882
  );
1789
1883
  }
1790
1884
 
1885
+ /** Past the empty-prompt guard — a model call is now guaranteed */
1886
+ if (contextUsage != null) {
1887
+ const usageRatio =
1888
+ contextUsage.calibrationRatio != null &&
1889
+ contextUsage.calibrationRatio > 0
1890
+ ? contextUsage.calibrationRatio
1891
+ : 1;
1892
+ if (
1893
+ agentContext.tokenCounter != null &&
1894
+ finalMessages.length !== messagesToUse.length
1895
+ ) {
1896
+ /** Post-prune formatting restructured the payload (e.g. thinking
1897
+ * placeholder collapse, orphan drops) — recount so the gauge
1898
+ * reflects what is actually sent */
1899
+ let rawTokens = 0;
1900
+ for (const message of finalMessages) {
1901
+ rawTokens += agentContext.tokenCounter(message);
1902
+ }
1903
+ contextUsage.breakdown.messageCount = finalMessages.length;
1904
+ if (
1905
+ contextUsage.contextBudget != null &&
1906
+ contextUsage.effectiveInstructionTokens != null
1907
+ ) {
1908
+ contextUsage.remainingContextTokens = Math.max(
1909
+ 0,
1910
+ contextUsage.contextBudget -
1911
+ contextUsage.effectiveInstructionTokens -
1912
+ Math.round(rawTokens * usageRatio)
1913
+ );
1914
+ }
1915
+ } else if (
1916
+ preFormatTailTokens != null &&
1917
+ agentContext.tokenCounter != null &&
1918
+ contextUsage.remainingContextTokens != null
1919
+ ) {
1920
+ /** Same-length formatting can still mutate in place — the trailing
1921
+ * tool batch (artifacts, Bedrock rewrites) and any legacy-converted
1922
+ * messages before it — adjust remaining by the calibrated delta */
1923
+ let postFormatTailTokens = 0;
1924
+ for (const message of finalMessages.slice(tailStart)) {
1925
+ postFormatTailTokens += agentContext.tokenCounter(message);
1926
+ }
1927
+ let formatDelta = postFormatTailTokens - preFormatTailTokens;
1928
+ if (legacyIndices != null && legacyIndices.length > 0) {
1929
+ let postFormatLegacyTokens = 0;
1930
+ for (const index of legacyIndices) {
1931
+ postFormatLegacyTokens += agentContext.tokenCounter(
1932
+ finalMessages[index]
1933
+ );
1934
+ }
1935
+ formatDelta += postFormatLegacyTokens - preFormatLegacyTokens;
1936
+ }
1937
+ if (formatDelta !== 0) {
1938
+ contextUsage.remainingContextTokens = Math.max(
1939
+ 0,
1940
+ Math.min(
1941
+ contextUsage.contextBudget ?? Number.MAX_SAFE_INTEGER,
1942
+ contextUsage.remainingContextTokens -
1943
+ Math.round(formatDelta * usageRatio)
1944
+ )
1945
+ );
1946
+ }
1947
+ }
1948
+ syncBudgetDerivedFields(contextUsage);
1949
+ /** Awaited so async host handlers receive the pre-invoke snapshot
1950
+ * before any model deltas are emitted */
1951
+ await safeDispatchCustomEvent(
1952
+ GraphEvents.ON_CONTEXT_USAGE,
1953
+ contextUsage,
1954
+ config
1955
+ );
1956
+ }
1957
+
1791
1958
  const invokeStart = Date.now();
1792
1959
  const invokeMeta = { runId: this.runId, agentId };
1793
1960
  emitAgentLog(
@@ -2063,6 +2230,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2063
2230
  parentAgentId: agentContext.agentId,
2064
2231
  langfuse: this.langfuse,
2065
2232
  tokenCounter: agentContext.tokenCounter,
2233
+ usageSink: this.subagentUsageSink,
2066
2234
  maxDepth: effectiveSubagentDepth,
2067
2235
  createChildGraph: (input): StandardGraph => {
2068
2236
  const childGraph = new StandardGraph(input);
package/src/index.ts CHANGED
@@ -8,6 +8,9 @@ export * from './messages';
8
8
  /* Graphs */
9
9
  export * from './graphs';
10
10
 
11
+ /* Context-usage projection (host-side pre-send snapshot) */
12
+ export * from './agents/projection';
13
+
11
14
  /* Summarization */
12
15
  export * from './summarization';
13
16
 
@@ -0,0 +1,317 @@
1
+ import { AIMessage, HumanMessage } from '@langchain/core/messages';
2
+ import type { BaseMessage } from '@langchain/core/messages';
3
+ import { _convertMessagesToAnthropicPayload } from './message_inputs';
4
+
5
+ /**
6
+ * Regression for cross-provider agent handoffs (e.g. Bedrock → Anthropic): a
7
+ * Bedrock turn that used extended thinking leaves a `reasoning_content` content
8
+ * block ({ reasoningText: { text, signature } }) in the history. The official
9
+ * Anthropic converter has no branch for it and previously threw
10
+ * "Unsupported message content format", crashing the handoff. Only known
11
+ * foreign reasoning (Bedrock `reasoning_content`, Google `reasoning`, LibreChat
12
+ * `think`) is dropped; any other unknown block still throws rather than being
13
+ * silently omitted (real content — user media, Google code-execution — must be
14
+ * surfaced); and a tool call carried only on `tool_calls` survives dropping its
15
+ * reasoning sibling without being duplicated.
16
+ */
17
+ type AnthropicPayload = ReturnType<typeof _convertMessagesToAnthropicPayload>;
18
+
19
+ /** Minimal view of a converted Anthropic content block the assertions read. */
20
+ interface TestBlock {
21
+ type?: string;
22
+ text?: string;
23
+ }
24
+
25
+ const findAssistant = (payload: AnthropicPayload) =>
26
+ payload.messages.find((m) => m.role === 'assistant');
27
+
28
+ const assistantBlocks = (payload: AnthropicPayload): TestBlock[] => {
29
+ const content = findAssistant(payload)?.content;
30
+ return Array.isArray(content) ? (content as TestBlock[]) : [];
31
+ };
32
+
33
+ describe('_convertMessagesToAnthropicPayload — cross-provider reasoning blocks', () => {
34
+ const bedrockHandoffHistory = (): BaseMessage[] => [
35
+ new HumanMessage('research Assort Health'),
36
+ new AIMessage({
37
+ content: [
38
+ {
39
+ type: 'reasoning_content',
40
+ index: 0,
41
+ reasoningText: {
42
+ text: 'Let me search Notion then hand off to the data agent.',
43
+ signature: 'bedrock-signature-not-valid-for-anthropic',
44
+ },
45
+ },
46
+ { type: 'text', text: 'Kicking off the searches now.' },
47
+ {
48
+ type: 'tool_use',
49
+ id: 'tooluse_abc',
50
+ name: 'notion-search',
51
+ input: { query: 'Assort Health' },
52
+ },
53
+ ],
54
+ tool_calls: [
55
+ {
56
+ id: 'tooluse_abc',
57
+ name: 'notion-search',
58
+ args: { query: 'Assort Health' },
59
+ type: 'tool_call',
60
+ },
61
+ ],
62
+ }),
63
+ ];
64
+
65
+ it('does not throw on a Bedrock reasoning_content block', () => {
66
+ expect(() =>
67
+ _convertMessagesToAnthropicPayload(bedrockHandoffHistory())
68
+ ).not.toThrow();
69
+ });
70
+
71
+ it('drops reasoning_content (incl. its foreign signature) but keeps text and tool_use', () => {
72
+ const payload = _convertMessagesToAnthropicPayload(bedrockHandoffHistory());
73
+ expect(findAssistant(payload)).toBeDefined();
74
+ const blocks = assistantBlocks(payload);
75
+
76
+ expect(blocks.find((b) => b.type === 'reasoning_content')).toBeUndefined();
77
+ expect(
78
+ blocks.find(
79
+ (b) => b.type === 'thinking' || b.type === 'redacted_thinking'
80
+ )
81
+ ).toBeUndefined();
82
+ expect(JSON.stringify(blocks)).not.toContain(
83
+ 'bedrock-signature-not-valid-for-anthropic'
84
+ );
85
+
86
+ expect(
87
+ blocks.some(
88
+ (b) => b.type === 'text' && b.text === 'Kicking off the searches now.'
89
+ )
90
+ ).toBe(true);
91
+ expect(blocks.find((b) => b.type === 'tool_use')).toMatchObject({
92
+ type: 'tool_use',
93
+ id: 'tooluse_abc',
94
+ name: 'notion-search',
95
+ input: { query: 'Assort Health' },
96
+ });
97
+ });
98
+
99
+ it('drops a Google `reasoning` block without throwing', () => {
100
+ const history: BaseMessage[] = [
101
+ new HumanMessage('hi'),
102
+ new AIMessage({
103
+ content: [
104
+ { type: 'reasoning', reasoning: 'internal google chain of thought' },
105
+ { type: 'text', text: 'Hello!' },
106
+ ],
107
+ }),
108
+ ];
109
+ expect(() => _convertMessagesToAnthropicPayload(history)).not.toThrow();
110
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
111
+ expect(blocks.find((b) => b.type === 'reasoning')).toBeUndefined();
112
+ expect(blocks.some((b) => b.type === 'text' && b.text === 'Hello!')).toBe(
113
+ true
114
+ );
115
+ });
116
+
117
+ it('drops a LibreChat `think` block without throwing', () => {
118
+ const history: BaseMessage[] = [
119
+ new HumanMessage('hi'),
120
+ new AIMessage({
121
+ content: [
122
+ { type: 'think', think: 'librechat serialized reasoning' },
123
+ { type: 'text', text: 'Done.' },
124
+ ],
125
+ }),
126
+ ];
127
+ expect(() => _convertMessagesToAnthropicPayload(history)).not.toThrow();
128
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
129
+ expect(blocks.find((b) => b.type === 'think')).toBeUndefined();
130
+ expect(blocks.some((b) => b.type === 'text' && b.text === 'Done.')).toBe(
131
+ true
132
+ );
133
+ });
134
+
135
+ it('drops an unsigned `thinking` block (Google thinking-enabled output) on an assistant turn', () => {
136
+ const history: BaseMessage[] = [
137
+ new HumanMessage('hi'),
138
+ new AIMessage({
139
+ content: [
140
+ {
141
+ type: 'thinking',
142
+ thinking: 'google chain of thought, no signature',
143
+ },
144
+ { type: 'text', text: 'Answer.' },
145
+ ],
146
+ }),
147
+ ];
148
+ expect(() => _convertMessagesToAnthropicPayload(history)).not.toThrow();
149
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
150
+ expect(blocks.find((b) => b.type === 'thinking')).toBeUndefined();
151
+ expect(blocks.some((b) => b.type === 'text' && b.text === 'Answer.')).toBe(
152
+ true
153
+ );
154
+ });
155
+
156
+ it('forwards a signed `thinking` block (Anthropic-native) unchanged', () => {
157
+ const history: BaseMessage[] = [
158
+ new HumanMessage('hi'),
159
+ new AIMessage({
160
+ content: [
161
+ {
162
+ type: 'thinking',
163
+ thinking: 'native reasoning',
164
+ signature: 'valid-sig',
165
+ },
166
+ { type: 'text', text: 'Answer.' },
167
+ ],
168
+ }),
169
+ ];
170
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
171
+ expect(blocks.find((b) => b.type === 'thinking')).toMatchObject({
172
+ type: 'thinking',
173
+ thinking: 'native reasoning',
174
+ signature: 'valid-sig',
175
+ });
176
+ });
177
+
178
+ it('throws (not silently drops) on an unknown assistant block such as Google code execution', () => {
179
+ // executableCode/codeExecutionResult carry real visible content; silently
180
+ // dropping them on a Google → Anthropic handoff would lose evidence.
181
+ const history: BaseMessage[] = [
182
+ new HumanMessage('run some code'),
183
+ new AIMessage({
184
+ content: [
185
+ {
186
+ type: 'executableCode',
187
+ executableCode: { language: 'PYTHON', code: 'print(2+2)' },
188
+ },
189
+ { type: 'text', text: 'Here is the result.' },
190
+ ],
191
+ }),
192
+ ];
193
+ expect(() => _convertMessagesToAnthropicPayload(history)).toThrow(
194
+ 'Unsupported message content format'
195
+ );
196
+ });
197
+
198
+ it('throws (not silently drops) on an unsupported user block such as media', () => {
199
+ const history: BaseMessage[] = [
200
+ new HumanMessage({
201
+ content: [
202
+ {
203
+ type: 'video_url',
204
+ video_url: { url: 'https://example.com/v.mp4' },
205
+ },
206
+ { type: 'text', text: 'what is in this video?' },
207
+ ],
208
+ }),
209
+ ];
210
+ expect(() => _convertMessagesToAnthropicPayload(history)).toThrow(
211
+ 'Unsupported message content format'
212
+ );
213
+ });
214
+
215
+ it('does not drop a reasoning-typed block on a user turn (only assistant reasoning is dropped)', () => {
216
+ const history: BaseMessage[] = [
217
+ new HumanMessage({
218
+ content: [
219
+ { type: 'reasoning_content', reasoningText: { text: 'user text' } },
220
+ { type: 'text', text: 'hello' },
221
+ ],
222
+ }),
223
+ ];
224
+ expect(() => _convertMessagesToAnthropicPayload(history)).toThrow(
225
+ 'Unsupported message content format'
226
+ );
227
+ });
228
+
229
+ it('preserves a tool call carried only on tool_calls when its reasoning sibling is dropped', () => {
230
+ // Mirrors a Bedrock extended-thinking turn: the tool lives only on
231
+ // `tool_calls`; `content` holds just the reasoning block (no tool_use).
232
+ const history: BaseMessage[] = [
233
+ new HumanMessage('research Assort Health'),
234
+ new AIMessage({
235
+ content: [
236
+ {
237
+ type: 'reasoning_content',
238
+ reasoningText: { text: 'I should hand off now.', signature: 'sig' },
239
+ },
240
+ ],
241
+ tool_calls: [
242
+ {
243
+ id: 'tooluse_transfer',
244
+ name: 'lc_transfer_to_data_agent',
245
+ args: { reason: 'need consumption data' },
246
+ type: 'tool_call',
247
+ },
248
+ ],
249
+ }),
250
+ ];
251
+ expect(() => _convertMessagesToAnthropicPayload(history)).not.toThrow();
252
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
253
+ expect(blocks.find((b) => b.type === 'reasoning_content')).toBeUndefined();
254
+ expect(blocks.find((b) => b.type === 'tool_use')).toMatchObject({
255
+ type: 'tool_use',
256
+ id: 'tooluse_transfer',
257
+ name: 'lc_transfer_to_data_agent',
258
+ input: { reason: 'need consumption data' },
259
+ });
260
+ // The `_` placeholder must not linger once a real tool_use block is present.
261
+ expect(blocks.some((b) => b.type === 'text' && b.text === '_')).toBe(false);
262
+ });
263
+
264
+ it('does not duplicate a Google functionCall tool call already materialized by _formatContent', () => {
265
+ // _formatContent converts the `functionCall` part into a tool_use; the
266
+ // materialization must recognize it as represented and not append a second.
267
+ const history: BaseMessage[] = [
268
+ new HumanMessage('weather in SF?'),
269
+ new AIMessage({
270
+ content: [
271
+ {
272
+ type: 'functionCall',
273
+ functionCall: { name: 'get_weather', args: { city: 'SF' } },
274
+ },
275
+ ],
276
+ tool_calls: [
277
+ {
278
+ id: 'call_weather_1',
279
+ name: 'get_weather',
280
+ args: { city: 'SF' },
281
+ type: 'tool_call',
282
+ },
283
+ ],
284
+ }),
285
+ ];
286
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
287
+ const toolUses = blocks.filter((b) => b.type === 'tool_use');
288
+ expect(toolUses).toHaveLength(1);
289
+ expect(toolUses[0]).toMatchObject({
290
+ type: 'tool_use',
291
+ id: 'call_weather_1',
292
+ name: 'get_weather',
293
+ });
294
+ });
295
+
296
+ it('falls back to placeholder text when reasoning was the only content', () => {
297
+ const history: BaseMessage[] = [
298
+ new HumanMessage('hi'),
299
+ new AIMessage({
300
+ content: [
301
+ {
302
+ type: 'reasoning_content',
303
+ reasoningText: {
304
+ text: 'only thinking, no visible text',
305
+ signature: 'sig',
306
+ },
307
+ },
308
+ ],
309
+ }),
310
+ ];
311
+ expect(() => _convertMessagesToAnthropicPayload(history)).not.toThrow();
312
+ const blocks = assistantBlocks(_convertMessagesToAnthropicPayload(history));
313
+ expect(blocks.find((b) => b.type === 'reasoning_content')).toBeUndefined();
314
+ expect(blocks.length).toBeGreaterThan(0);
315
+ expect(blocks.every((b) => b.type === 'text')).toBe(true);
316
+ });
317
+ });