@librechat/agents 3.1.78 → 3.1.79
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/llm/anthropic/index.cjs +44 -55
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +97 -14
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +43 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +48 -15
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +97 -14
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/types/llm/anthropic/index.d.ts +1 -9
- package/dist/types/messages/anthropicToolCache.d.ts +5 -5
- package/package.json +1 -1
- package/src/llm/anthropic/index.ts +55 -64
- package/src/llm/anthropic/llm.spec.ts +585 -0
- package/src/llm/anthropic/utils/message_inputs.ts +36 -21
- package/src/llm/anthropic/utils/message_outputs.ts +0 -4
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
- package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
- package/src/messages/anthropicToolCache.ts +70 -25
- package/src/messages/format.ts +117 -18
- package/src/messages/formatAgentMessages.test.ts +202 -1
- package/src/specs/summarization.test.ts +3 -3
- package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
- package/src/tools/local/LocalExecutionEngine.ts +55 -54
- package/src/types/diff.d.ts +15 -0
|
@@ -31,16 +31,39 @@ import type {
|
|
|
31
31
|
MessageContentComplex,
|
|
32
32
|
} from '@langchain/core/messages';
|
|
33
33
|
import { toLangChainContent } from '@/messages/langchain';
|
|
34
|
+
import { formatAgentMessages } from '@/messages/format';
|
|
35
|
+
import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
|
|
34
36
|
import { _documentsInParams, CustomAnthropic as ChatAnthropic } from './index';
|
|
37
|
+
import { partitionAndMarkAnthropicToolCache } from '@/messages/anthropicToolCache';
|
|
38
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
39
|
+
import { ModelEndHandler, ToolEndHandler } from '@/events';
|
|
40
|
+
import { Run } from '@/run';
|
|
35
41
|
import type { CustomAnthropicCallOptions } from './index';
|
|
36
42
|
import type {
|
|
37
43
|
AnthropicContextManagementConfigParam,
|
|
38
44
|
AnthropicMessageCreateParams,
|
|
45
|
+
AnthropicMessageStreamEvent,
|
|
39
46
|
AnthropicMessageResponse,
|
|
40
47
|
AnthropicOutputConfig,
|
|
48
|
+
AnthropicRequestOptions,
|
|
49
|
+
AnthropicStreamingMessageCreateParams,
|
|
41
50
|
AnthropicThinkingConfigParam,
|
|
42
51
|
ChatAnthropicContentBlock,
|
|
43
52
|
} from './types';
|
|
53
|
+
import type {
|
|
54
|
+
AnthropicClientOptions,
|
|
55
|
+
IState,
|
|
56
|
+
MessageContentComplex as LibreChatContentBlock,
|
|
57
|
+
MessageDeltaEvent,
|
|
58
|
+
ReasoningDeltaEvent,
|
|
59
|
+
RunConfig,
|
|
60
|
+
RunStep,
|
|
61
|
+
RunStepDeltaEvent,
|
|
62
|
+
SharedLLMConfig,
|
|
63
|
+
StreamEventData,
|
|
64
|
+
ToolEndEvent,
|
|
65
|
+
TPayload,
|
|
66
|
+
} from '@/types';
|
|
44
67
|
import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
|
|
45
68
|
import {
|
|
46
69
|
_makeMessageChunkFromAnthropicEvent,
|
|
@@ -86,6 +109,8 @@ const remoteImageUrl =
|
|
|
86
109
|
|
|
87
110
|
// Use this model for all other tests
|
|
88
111
|
const modelName = 'claude-haiku-4-5-20251001';
|
|
112
|
+
const webSearchModelName =
|
|
113
|
+
process.env.ANTHROPIC_WEB_SEARCH_MODEL ?? 'claude-opus-4-7';
|
|
89
114
|
|
|
90
115
|
type AnthropicThinkingResponseBlock = Anthropic.Messages.ThinkingBlock & {
|
|
91
116
|
index?: number;
|
|
@@ -119,6 +144,12 @@ type CompactionContentBlock = ContentBlock & {
|
|
|
119
144
|
content: string;
|
|
120
145
|
};
|
|
121
146
|
|
|
147
|
+
type AnthropicContentBlockWithId = ContentBlock & {
|
|
148
|
+
id?: unknown;
|
|
149
|
+
input?: unknown;
|
|
150
|
+
name?: unknown;
|
|
151
|
+
};
|
|
152
|
+
|
|
122
153
|
function getLangChainErrorCode(error: unknown): string | undefined {
|
|
123
154
|
if (typeof error !== 'object' || error == null) {
|
|
124
155
|
return undefined;
|
|
@@ -200,6 +231,184 @@ function isCompactionBlock(
|
|
|
200
231
|
return typeof content === 'string';
|
|
201
232
|
}
|
|
202
233
|
|
|
234
|
+
function isServerToolUseBlock(
|
|
235
|
+
block: ContentBlock
|
|
236
|
+
): block is AnthropicContentBlockWithId {
|
|
237
|
+
return (
|
|
238
|
+
block.type === 'server_tool_use' &&
|
|
239
|
+
typeof (block as AnthropicContentBlockWithId).id === 'string' &&
|
|
240
|
+
((block as AnthropicContentBlockWithId).id as string).startsWith(
|
|
241
|
+
Constants.ANTHROPIC_SERVER_TOOL_PREFIX
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function expectAnthropicPayloadContentIsNonEmpty(
|
|
247
|
+
payload: AnthropicMessageCreateParams
|
|
248
|
+
): void {
|
|
249
|
+
for (const message of payload.messages) {
|
|
250
|
+
if (typeof message.content === 'string') {
|
|
251
|
+
expect(message.content.trim().length).toBeGreaterThan(0);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
expect(message.content.length).toBeGreaterThan(0);
|
|
256
|
+
for (const block of message.content) {
|
|
257
|
+
if (block.type !== 'text') {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
expect(block.text.trim().length).toBeGreaterThan(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function expectNoDanglingServerToolUses(
|
|
266
|
+
payload: AnthropicMessageCreateParams
|
|
267
|
+
): void {
|
|
268
|
+
for (const message of payload.messages) {
|
|
269
|
+
if (typeof message.content === 'string') {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const serverToolResultIds = new Set(
|
|
274
|
+
message.content
|
|
275
|
+
.map((block) =>
|
|
276
|
+
'tool_use_id' in block &&
|
|
277
|
+
typeof block.tool_use_id === 'string' &&
|
|
278
|
+
block.tool_use_id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
|
|
279
|
+
? block.tool_use_id
|
|
280
|
+
: undefined
|
|
281
|
+
)
|
|
282
|
+
.filter((id): id is string => id != null)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
for (const block of message.content) {
|
|
286
|
+
if (block.type !== 'server_tool_use') {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
expect(serverToolResultIds.has(block.id)).toBe(true);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getPromptCachedWebSearchTools(): Parameters<
|
|
295
|
+
ChatAnthropic['bindTools']
|
|
296
|
+
>[0] {
|
|
297
|
+
const tools = partitionAndMarkAnthropicToolCache(
|
|
298
|
+
[
|
|
299
|
+
{
|
|
300
|
+
type: 'web_search_20250305',
|
|
301
|
+
name: 'web_search',
|
|
302
|
+
max_uses: 3,
|
|
303
|
+
},
|
|
304
|
+
] as never,
|
|
305
|
+
() => false
|
|
306
|
+
);
|
|
307
|
+
return tools as Parameters<ChatAnthropic['bindTools']>[0];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getWebSearchTool(): {
|
|
311
|
+
type: 'web_search_20250305';
|
|
312
|
+
name: 'web_search';
|
|
313
|
+
max_uses: number;
|
|
314
|
+
} {
|
|
315
|
+
return {
|
|
316
|
+
type: 'web_search_20250305',
|
|
317
|
+
name: 'web_search',
|
|
318
|
+
max_uses: 3,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getWebSearchLLMConfig(): AnthropicClientOptions & SharedLLMConfig {
|
|
323
|
+
return {
|
|
324
|
+
provider: Providers.ANTHROPIC,
|
|
325
|
+
model: webSearchModelName,
|
|
326
|
+
maxTokens: 1024,
|
|
327
|
+
promptCache: true,
|
|
328
|
+
streaming: true,
|
|
329
|
+
streamUsage: true,
|
|
330
|
+
thinking: { type: 'adaptive' },
|
|
331
|
+
} as AnthropicClientOptions & SharedLLMConfig;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function createWebSearchRun({
|
|
335
|
+
runId,
|
|
336
|
+
customHandlers,
|
|
337
|
+
}: {
|
|
338
|
+
runId: string;
|
|
339
|
+
customHandlers?: RunConfig['customHandlers'];
|
|
340
|
+
}): Promise<Run<IState>> {
|
|
341
|
+
return await Run.create<IState>({
|
|
342
|
+
runId,
|
|
343
|
+
graphConfig: {
|
|
344
|
+
type: 'standard',
|
|
345
|
+
llmConfig: getWebSearchLLMConfig(),
|
|
346
|
+
tools: [getWebSearchTool()],
|
|
347
|
+
instructions:
|
|
348
|
+
'You are a concise assistant. Use web search when current facts are needed.',
|
|
349
|
+
},
|
|
350
|
+
returnContent: true,
|
|
351
|
+
skipCleanup: true,
|
|
352
|
+
customHandlers,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function createLibreChatContentHandlers(): {
|
|
357
|
+
aggregateContent: ReturnType<
|
|
358
|
+
typeof createContentAggregator
|
|
359
|
+
>['aggregateContent'];
|
|
360
|
+
contentParts: Array<LibreChatContentBlock | undefined>;
|
|
361
|
+
customHandlers: NonNullable<RunConfig['customHandlers']>;
|
|
362
|
+
} {
|
|
363
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
364
|
+
const customHandlers = {
|
|
365
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
366
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
367
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
368
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
369
|
+
handle: (
|
|
370
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
371
|
+
data: StreamEventData
|
|
372
|
+
): void => {
|
|
373
|
+
aggregateContent({
|
|
374
|
+
event,
|
|
375
|
+
data: data as unknown as { result: ToolEndEvent },
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
380
|
+
handle: (event: GraphEvents.ON_RUN_STEP, data: StreamEventData): void => {
|
|
381
|
+
aggregateContent({ event, data: data as RunStep });
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
385
|
+
handle: (
|
|
386
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
387
|
+
data: StreamEventData
|
|
388
|
+
): void => {
|
|
389
|
+
aggregateContent({ event, data: data as RunStepDeltaEvent });
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
393
|
+
handle: (
|
|
394
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
395
|
+
data: StreamEventData
|
|
396
|
+
): void => {
|
|
397
|
+
aggregateContent({ event, data: data as MessageDeltaEvent });
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
[GraphEvents.ON_REASONING_DELTA]: {
|
|
401
|
+
handle: (
|
|
402
|
+
event: GraphEvents.ON_REASONING_DELTA,
|
|
403
|
+
data: StreamEventData
|
|
404
|
+
): void => {
|
|
405
|
+
aggregateContent({ event, data: data as ReasoningDeltaEvent });
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
return { aggregateContent, contentParts, customHandlers };
|
|
410
|
+
}
|
|
411
|
+
|
|
203
412
|
test('Test ChatAnthropic', async () => {
|
|
204
413
|
const chat = new ChatAnthropic({
|
|
205
414
|
modelName,
|
|
@@ -691,6 +900,287 @@ test('Anthropic usage metadata includes cache input token buckets', () => {
|
|
|
691
900
|
});
|
|
692
901
|
});
|
|
693
902
|
|
|
903
|
+
type AnthropicStreamEvent = Anthropic.Beta.Messages.BetaRawMessageStreamEvent;
|
|
904
|
+
|
|
905
|
+
function createMockAnthropicStream(events: AnthropicStreamEvent[]) {
|
|
906
|
+
return {
|
|
907
|
+
controller: { abort: jest.fn() },
|
|
908
|
+
async *[Symbol.asyncIterator]() {
|
|
909
|
+
for (const event of events) {
|
|
910
|
+
yield event;
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
class MockStreamingAnthropic extends ChatAnthropic {
|
|
917
|
+
constructor(private readonly mockEvents: AnthropicStreamEvent[]) {
|
|
918
|
+
super({
|
|
919
|
+
modelName,
|
|
920
|
+
apiKey: 'test-key',
|
|
921
|
+
maxTokens: 10,
|
|
922
|
+
streamUsage: true,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
protected override async createStreamWithRetry() {
|
|
927
|
+
return createMockAnthropicStream(this.mockEvents) as never;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
class RecordingStreamingAnthropic extends ChatAnthropic {
|
|
932
|
+
messageStartOutputTokens = 0;
|
|
933
|
+
readonly messageDeltaOutputTokens: number[] = [];
|
|
934
|
+
|
|
935
|
+
protected override async createStreamWithRetry(
|
|
936
|
+
request: AnthropicStreamingMessageCreateParams,
|
|
937
|
+
options?: AnthropicRequestOptions
|
|
938
|
+
) {
|
|
939
|
+
const stream = await super.createStreamWithRetry(request, options);
|
|
940
|
+
const recorder = this;
|
|
941
|
+
|
|
942
|
+
return {
|
|
943
|
+
controller: stream.controller,
|
|
944
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<AnthropicMessageStreamEvent> {
|
|
945
|
+
for await (const event of stream) {
|
|
946
|
+
if (event.type === 'message_start') {
|
|
947
|
+
recorder.messageStartOutputTokens =
|
|
948
|
+
event.message.usage.output_tokens ??
|
|
949
|
+
recorder.messageStartOutputTokens;
|
|
950
|
+
} else if (event.type === 'message_delta') {
|
|
951
|
+
recorder.messageDeltaOutputTokens.push(event.usage.output_tokens);
|
|
952
|
+
}
|
|
953
|
+
yield event;
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
} as unknown as typeof stream;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
test('Anthropic message_delta usage emits only output token totals', () => {
|
|
961
|
+
const event: AnthropicStreamEvent = {
|
|
962
|
+
type: 'message_delta',
|
|
963
|
+
context_management: null,
|
|
964
|
+
delta: {
|
|
965
|
+
container: null,
|
|
966
|
+
stop_details: null,
|
|
967
|
+
stop_reason: 'end_turn',
|
|
968
|
+
stop_sequence: null,
|
|
969
|
+
},
|
|
970
|
+
usage: {
|
|
971
|
+
input_tokens: 243,
|
|
972
|
+
output_tokens: 375,
|
|
973
|
+
cache_creation_input_tokens: 11,
|
|
974
|
+
cache_read_input_tokens: 13,
|
|
975
|
+
server_tool_use: null,
|
|
976
|
+
iterations: null,
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const result = _makeMessageChunkFromAnthropicEvent(event, {
|
|
981
|
+
streamUsage: true,
|
|
982
|
+
coerceContentToString: true,
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
expect(result?.chunk.usage_metadata).toEqual({
|
|
986
|
+
input_tokens: 0,
|
|
987
|
+
output_tokens: 375,
|
|
988
|
+
total_tokens: 375,
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test('Anthropic stream usage does not double-count cumulative input tokens', async () => {
|
|
993
|
+
const events: AnthropicStreamEvent[] = [
|
|
994
|
+
{
|
|
995
|
+
type: 'message_start',
|
|
996
|
+
message: {
|
|
997
|
+
id: 'msg_token_accounting',
|
|
998
|
+
container: null,
|
|
999
|
+
context_management: null,
|
|
1000
|
+
content: [],
|
|
1001
|
+
model: modelName,
|
|
1002
|
+
role: 'assistant',
|
|
1003
|
+
stop_details: null,
|
|
1004
|
+
stop_reason: null,
|
|
1005
|
+
stop_sequence: null,
|
|
1006
|
+
type: 'message',
|
|
1007
|
+
usage: {
|
|
1008
|
+
cache_creation: null,
|
|
1009
|
+
cache_creation_input_tokens: 0,
|
|
1010
|
+
cache_read_input_tokens: 0,
|
|
1011
|
+
inference_geo: null,
|
|
1012
|
+
input_tokens: 243,
|
|
1013
|
+
iterations: null,
|
|
1014
|
+
output_tokens: 0,
|
|
1015
|
+
server_tool_use: null,
|
|
1016
|
+
service_tier: null,
|
|
1017
|
+
speed: null,
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
type: 'message_delta',
|
|
1023
|
+
context_management: null,
|
|
1024
|
+
delta: {
|
|
1025
|
+
container: null,
|
|
1026
|
+
stop_details: null,
|
|
1027
|
+
stop_reason: 'end_turn',
|
|
1028
|
+
stop_sequence: null,
|
|
1029
|
+
},
|
|
1030
|
+
usage: {
|
|
1031
|
+
input_tokens: 243,
|
|
1032
|
+
output_tokens: 375,
|
|
1033
|
+
cache_creation_input_tokens: 0,
|
|
1034
|
+
cache_read_input_tokens: 0,
|
|
1035
|
+
server_tool_use: null,
|
|
1036
|
+
iterations: null,
|
|
1037
|
+
},
|
|
1038
|
+
},
|
|
1039
|
+
{ type: 'message_stop' },
|
|
1040
|
+
];
|
|
1041
|
+
const model = new MockStreamingAnthropic(events);
|
|
1042
|
+
|
|
1043
|
+
let full: AIMessageChunk | undefined;
|
|
1044
|
+
for await (const chunk of await model.stream('hello')) {
|
|
1045
|
+
full = !full ? chunk : concat(full, chunk);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
expect(full?.usage_metadata).toEqual({
|
|
1049
|
+
input_tokens: 243,
|
|
1050
|
+
output_tokens: 375,
|
|
1051
|
+
total_tokens: 618,
|
|
1052
|
+
input_token_details: {
|
|
1053
|
+
cache_creation: 0,
|
|
1054
|
+
cache_read: 0,
|
|
1055
|
+
},
|
|
1056
|
+
output_token_details: {},
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test('Anthropic stream usage handles multiple cumulative message_delta events', async () => {
|
|
1061
|
+
const events: AnthropicStreamEvent[] = [
|
|
1062
|
+
{
|
|
1063
|
+
type: 'message_start',
|
|
1064
|
+
message: {
|
|
1065
|
+
id: 'msg_token_accounting_multi_delta',
|
|
1066
|
+
container: null,
|
|
1067
|
+
context_management: null,
|
|
1068
|
+
content: [],
|
|
1069
|
+
model: modelName,
|
|
1070
|
+
role: 'assistant',
|
|
1071
|
+
stop_details: null,
|
|
1072
|
+
stop_reason: null,
|
|
1073
|
+
stop_sequence: null,
|
|
1074
|
+
type: 'message',
|
|
1075
|
+
usage: {
|
|
1076
|
+
cache_creation: null,
|
|
1077
|
+
cache_creation_input_tokens: 0,
|
|
1078
|
+
cache_read_input_tokens: 0,
|
|
1079
|
+
inference_geo: null,
|
|
1080
|
+
input_tokens: 243,
|
|
1081
|
+
iterations: null,
|
|
1082
|
+
output_tokens: 0,
|
|
1083
|
+
server_tool_use: null,
|
|
1084
|
+
service_tier: null,
|
|
1085
|
+
speed: null,
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
type: 'message_delta',
|
|
1091
|
+
context_management: null,
|
|
1092
|
+
delta: {
|
|
1093
|
+
container: null,
|
|
1094
|
+
stop_details: null,
|
|
1095
|
+
stop_reason: null,
|
|
1096
|
+
stop_sequence: null,
|
|
1097
|
+
},
|
|
1098
|
+
usage: {
|
|
1099
|
+
input_tokens: 243,
|
|
1100
|
+
output_tokens: 100,
|
|
1101
|
+
cache_creation_input_tokens: 0,
|
|
1102
|
+
cache_read_input_tokens: 0,
|
|
1103
|
+
server_tool_use: null,
|
|
1104
|
+
iterations: null,
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
type: 'message_delta',
|
|
1109
|
+
context_management: null,
|
|
1110
|
+
delta: {
|
|
1111
|
+
container: null,
|
|
1112
|
+
stop_details: null,
|
|
1113
|
+
stop_reason: 'end_turn',
|
|
1114
|
+
stop_sequence: null,
|
|
1115
|
+
},
|
|
1116
|
+
usage: {
|
|
1117
|
+
input_tokens: 243,
|
|
1118
|
+
output_tokens: 375,
|
|
1119
|
+
cache_creation_input_tokens: 0,
|
|
1120
|
+
cache_read_input_tokens: 0,
|
|
1121
|
+
server_tool_use: null,
|
|
1122
|
+
iterations: null,
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
{ type: 'message_stop' },
|
|
1126
|
+
];
|
|
1127
|
+
const model = new MockStreamingAnthropic(events);
|
|
1128
|
+
|
|
1129
|
+
let full: AIMessageChunk | undefined;
|
|
1130
|
+
for await (const chunk of await model.stream('hello')) {
|
|
1131
|
+
full = !full ? chunk : concat(full, chunk);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
expect(full?.usage_metadata).toEqual({
|
|
1135
|
+
input_tokens: 243,
|
|
1136
|
+
output_tokens: 375,
|
|
1137
|
+
total_tokens: 618,
|
|
1138
|
+
input_token_details: {
|
|
1139
|
+
cache_creation: 0,
|
|
1140
|
+
cache_read: 0,
|
|
1141
|
+
},
|
|
1142
|
+
output_token_details: {},
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test('Anthropic live stream usage matches raw cumulative output snapshots', async () => {
|
|
1147
|
+
const model = new RecordingStreamingAnthropic({
|
|
1148
|
+
modelName,
|
|
1149
|
+
temperature: 0,
|
|
1150
|
+
maxTokens: 500,
|
|
1151
|
+
_lc_stream_delay: 0,
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
let full: AIMessageChunk | undefined;
|
|
1155
|
+
const stream = await model.stream(
|
|
1156
|
+
'Write exactly 18 numbered lines about reliable software telemetry. Each line should contain exactly seven words. Do not add an intro or outro.'
|
|
1157
|
+
);
|
|
1158
|
+
for await (const chunk of stream) {
|
|
1159
|
+
full = !full ? chunk : concat(full, chunk);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
expect(model.messageDeltaOutputTokens.length).toBeGreaterThan(0);
|
|
1163
|
+
const rawOutputTokens =
|
|
1164
|
+
model.messageDeltaOutputTokens[model.messageDeltaOutputTokens.length - 1];
|
|
1165
|
+
expect(full?.usage_metadata?.output_tokens).toBe(
|
|
1166
|
+
model.messageStartOutputTokens + rawOutputTokens
|
|
1167
|
+
);
|
|
1168
|
+
expect(full?.usage_metadata?.total_tokens).toBe(
|
|
1169
|
+
(full?.usage_metadata?.input_tokens ?? 0) +
|
|
1170
|
+
(full?.usage_metadata?.output_tokens ?? 0)
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
if (model.messageDeltaOutputTokens.length > 1) {
|
|
1174
|
+
const summedOutputTokens = model.messageDeltaOutputTokens.reduce(
|
|
1175
|
+
(sum, tokens) => sum + tokens,
|
|
1176
|
+
0
|
|
1177
|
+
);
|
|
1178
|
+
expect(full?.usage_metadata?.output_tokens).toBeLessThan(
|
|
1179
|
+
model.messageStartOutputTokens + summedOutputTokens
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
|
|
694
1184
|
test('document detection ignores null content placeholders', () => {
|
|
695
1185
|
const params: AnthropicMessageCreateParams = {
|
|
696
1186
|
model: modelName,
|
|
@@ -1166,6 +1656,101 @@ test('human message caching', async () => {
|
|
|
1166
1656
|
);
|
|
1167
1657
|
});
|
|
1168
1658
|
|
|
1659
|
+
describe('Anthropic web search live regressions', () => {
|
|
1660
|
+
test('accepts prompt-cache markers on built-in web search tools', async () => {
|
|
1661
|
+
const model = new ChatAnthropic({
|
|
1662
|
+
model: webSearchModelName,
|
|
1663
|
+
maxTokens: 1024,
|
|
1664
|
+
thinking: { type: 'adaptive' },
|
|
1665
|
+
});
|
|
1666
|
+
const tools = getPromptCachedWebSearchTools();
|
|
1667
|
+
const formattedTools = model.formatStructuredToolToAnthropic(tools);
|
|
1668
|
+
|
|
1669
|
+
expect(formattedTools?.[0]).toMatchObject({
|
|
1670
|
+
type: 'web_search_20250305',
|
|
1671
|
+
name: 'web_search',
|
|
1672
|
+
cache_control: { type: 'ephemeral' },
|
|
1673
|
+
});
|
|
1674
|
+
expect(formattedTools?.[0]).not.toHaveProperty('extras');
|
|
1675
|
+
|
|
1676
|
+
const response = await model
|
|
1677
|
+
.bindTools(tools)
|
|
1678
|
+
.invoke([
|
|
1679
|
+
new HumanMessage(
|
|
1680
|
+
'Use web search once and answer with only the word: ok'
|
|
1681
|
+
),
|
|
1682
|
+
]);
|
|
1683
|
+
|
|
1684
|
+
expect(response.content.length).toBeGreaterThan(0);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
test('replays LibreChat-persisted web search content across runs', async () => {
|
|
1688
|
+
const threadId = `web-search-e2e-${Date.now()}`;
|
|
1689
|
+
const firstPrompt =
|
|
1690
|
+
'Use web search. Who is the lowest seed survived in 2026 NBA playoffs? Answer with only the team name.';
|
|
1691
|
+
const followUpPrompt = "Who are 76ers' opponents in current series?";
|
|
1692
|
+
const { contentParts: firstContentParts, customHandlers: firstHandlers } =
|
|
1693
|
+
createLibreChatContentHandlers();
|
|
1694
|
+
const firstRun = await createWebSearchRun({
|
|
1695
|
+
runId: `${threadId}-turn-1`,
|
|
1696
|
+
customHandlers: firstHandlers,
|
|
1697
|
+
});
|
|
1698
|
+
const runConfig = {
|
|
1699
|
+
configurable: { provider: Providers.ANTHROPIC, thread_id: threadId },
|
|
1700
|
+
streamMode: 'values',
|
|
1701
|
+
version: 'v2' as const,
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
const firstRunContent = await firstRun.processStream(
|
|
1705
|
+
{ messages: [new HumanMessage(firstPrompt)] },
|
|
1706
|
+
runConfig
|
|
1707
|
+
);
|
|
1708
|
+
const persistedAssistantContent = firstContentParts.filter(
|
|
1709
|
+
(part): part is LibreChatContentBlock => part != null
|
|
1710
|
+
);
|
|
1711
|
+
const hasPersistedServerToolCall = persistedAssistantContent.some(
|
|
1712
|
+
(part) =>
|
|
1713
|
+
part.type === ContentTypes.TOOL_CALL &&
|
|
1714
|
+
typeof part.tool_call?.id === 'string' &&
|
|
1715
|
+
part.tool_call.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
|
|
1716
|
+
);
|
|
1717
|
+
const hasPersistedAnswerText = persistedAssistantContent.some(
|
|
1718
|
+
(part) =>
|
|
1719
|
+
part.type === ContentTypes.TEXT &&
|
|
1720
|
+
typeof part.text === 'string' &&
|
|
1721
|
+
part.text.trim().length > 0
|
|
1722
|
+
);
|
|
1723
|
+
|
|
1724
|
+
expect(firstRunContent).toBeDefined();
|
|
1725
|
+
expect(persistedAssistantContent.length).toBeGreaterThan(0);
|
|
1726
|
+
expect(hasPersistedServerToolCall).toBe(true);
|
|
1727
|
+
expect(hasPersistedAnswerText).toBe(true);
|
|
1728
|
+
|
|
1729
|
+
const persistedPayload: TPayload = [
|
|
1730
|
+
{ role: 'user', content: firstPrompt },
|
|
1731
|
+
{ role: 'assistant', content: persistedAssistantContent },
|
|
1732
|
+
{ role: 'user', content: followUpPrompt },
|
|
1733
|
+
];
|
|
1734
|
+
const { messages } = formatAgentMessages(
|
|
1735
|
+
persistedPayload,
|
|
1736
|
+
undefined,
|
|
1737
|
+
new Set(['web_search']),
|
|
1738
|
+
undefined,
|
|
1739
|
+
{ provider: Providers.ANTHROPIC }
|
|
1740
|
+
);
|
|
1741
|
+
const anthropicPayload = _convertMessagesToAnthropicPayload(messages);
|
|
1742
|
+
const secondRun = await createWebSearchRun({
|
|
1743
|
+
runId: `${threadId}-turn-2`,
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
expectAnthropicPayloadContentIsNonEmpty(anthropicPayload);
|
|
1747
|
+
expectNoDanglingServerToolUses(anthropicPayload);
|
|
1748
|
+
await expect(
|
|
1749
|
+
secondRun.processStream({ messages }, runConfig)
|
|
1750
|
+
).resolves.toBeDefined();
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1169
1754
|
test('Can accept PDF documents', async () => {
|
|
1170
1755
|
const model = new ChatAnthropic({
|
|
1171
1756
|
modelName: pdfModelName,
|