@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.
- package/dist/cjs/agents/AgentContext.cjs +119 -9
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/agents/projection.cjs +25 -0
- package/dist/cjs/agents/projection.cjs.map +1 -0
- package/dist/cjs/common/enum.cjs +13 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +106 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +26 -4
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +20 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +49 -8
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +7 -0
- package/dist/cjs/messages/budget.cjs +23 -0
- package/dist/cjs/messages/budget.cjs.map +1 -0
- package/dist/cjs/messages/cache.cjs +1 -0
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/content.cjs +12 -14
- package/dist/cjs/messages/content.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -0
- package/dist/cjs/messages/prune.cjs +31 -13
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +12 -1
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/search/format.cjs +91 -2
- package/dist/cjs/tools/search/format.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +138 -2
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +30 -0
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +121 -11
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/agents/projection.mjs +25 -0
- package/dist/esm/agents/projection.mjs.map +1 -0
- package/dist/esm/common/enum.mjs +13 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +107 -4
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +26 -4
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +20 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +49 -8
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +6 -4
- package/dist/esm/messages/budget.mjs +23 -0
- package/dist/esm/messages/budget.mjs.map +1 -0
- package/dist/esm/messages/cache.mjs +1 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/content.mjs +12 -15
- package/dist/esm/messages/content.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -0
- package/dist/esm/messages/prune.mjs +31 -13
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +12 -1
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/search/format.mjs +91 -2
- package/dist/esm/tools/search/format.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +138 -2
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +30 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +37 -4
- package/dist/types/agents/projection.d.ts +26 -0
- package/dist/types/common/enum.d.ts +13 -0
- package/dist/types/graphs/Graph.d.ts +8 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/invoke.d.ts +1 -1
- package/dist/types/messages/budget.d.ts +11 -0
- package/dist/types/messages/cache.d.ts +7 -0
- package/dist/types/messages/content.d.ts +5 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/prune.d.ts +4 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/search/format.d.ts +4 -1
- package/dist/types/tools/search/types.d.ts +7 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +11 -1
- package/dist/types/types/graph.d.ts +89 -3
- package/dist/types/types/run.d.ts +13 -0
- package/dist/types/utils/tokens.d.ts +7 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +172 -8
- package/src/agents/__tests__/AgentContext.test.ts +235 -2
- package/src/agents/__tests__/projection.test.ts +73 -0
- package/src/agents/projection.ts +46 -0
- package/src/common/enum.ts +13 -0
- package/src/graphs/Graph.ts +168 -0
- package/src/index.ts +3 -0
- package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +317 -0
- package/src/llm/anthropic/utils/message_inputs.ts +78 -16
- package/src/llm/bedrock/utils/cross-provider-reasoning.test.ts +131 -0
- package/src/llm/bedrock/utils/message_inputs.ts +35 -0
- package/src/llm/invoke.test.ts +79 -1
- package/src/llm/invoke.ts +58 -4
- package/src/messages/budget.ts +32 -0
- package/src/messages/cache.ts +1 -1
- package/src/messages/content.ts +24 -32
- package/src/messages/index.ts +1 -0
- package/src/messages/prune.ts +39 -2
- package/src/run.ts +5 -0
- package/src/scripts/subagent-usage-sink.ts +176 -0
- package/src/specs/context-accuracy.live.test.ts +409 -0
- package/src/specs/context-usage-event.test.ts +117 -0
- package/src/specs/context-usage.live.test.ts +297 -0
- package/src/specs/prune.test.ts +51 -1
- package/src/specs/subagent.test.ts +124 -1
- package/src/summarization/__tests__/node.test.ts +60 -1
- package/src/summarization/node.ts +20 -1
- package/src/tools/__tests__/SubagentExecutor.test.ts +443 -1
- package/src/tools/search/format.test.ts +242 -0
- package/src/tools/search/format.ts +122 -5
- package/src/tools/search/tool.ts +5 -1
- package/src/tools/search/types.ts +7 -0
- package/src/tools/subagent/SubagentExecutor.ts +221 -3
- package/src/types/graph.ts +94 -1
- package/src/types/run.ts +13 -0
- package/src/utils/__tests__/apportion.test.ts +32 -0
- package/src/utils/tokens.ts +33 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
@@ -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
|
+
});
|