@librechat/agents 3.2.64 → 3.2.65

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 (61) hide show
  1. package/dist/cjs/graphs/Graph.cjs +3 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +73 -9
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +41 -9
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +3 -1
  9. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/utils/stream_events.cjs +337 -0
  11. package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -0
  12. package/dist/cjs/tools/search/crw-scraper.cjs +165 -0
  13. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -0
  14. package/dist/cjs/tools/search/crw-search.cjs +105 -0
  15. package/dist/cjs/tools/search/crw-search.cjs.map +1 -0
  16. package/dist/cjs/tools/search/search.cjs +4 -2
  17. package/dist/cjs/tools/search/search.cjs.map +1 -1
  18. package/dist/cjs/tools/search/tool.cjs +15 -3
  19. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +3 -0
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/llm/anthropic/index.mjs +73 -9
  23. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  24. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  25. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +41 -9
  26. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  27. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +3 -2
  28. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  29. package/dist/esm/llm/anthropic/utils/stream_events.mjs +337 -0
  30. package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -0
  31. package/dist/esm/tools/search/crw-scraper.mjs +163 -0
  32. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -0
  33. package/dist/esm/tools/search/crw-search.mjs +103 -0
  34. package/dist/esm/tools/search/crw-search.mjs.map +1 -0
  35. package/dist/esm/tools/search/search.mjs +4 -2
  36. package/dist/esm/tools/search/search.mjs.map +1 -1
  37. package/dist/esm/tools/search/tool.mjs +15 -3
  38. package/dist/esm/tools/search/tool.mjs.map +1 -1
  39. package/dist/types/llm/anthropic/index.d.ts +2 -0
  40. package/dist/types/llm/anthropic/types.d.ts +2 -0
  41. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -2
  42. package/dist/types/llm/anthropic/utils/stream_events.d.ts +25 -0
  43. package/dist/types/tools/search/crw-scraper.d.ts +41 -0
  44. package/dist/types/tools/search/crw-search.d.ts +4 -0
  45. package/dist/types/tools/search/types.d.ts +88 -3
  46. package/package.json +1 -1
  47. package/src/graphs/Graph.ts +15 -0
  48. package/src/llm/anthropic/index.ts +134 -10
  49. package/src/llm/anthropic/inherited-content-utils.spec.ts +10 -5
  50. package/src/llm/anthropic/inherited-stream-events.spec.ts +513 -9
  51. package/src/llm/anthropic/llm.spec.ts +100 -16
  52. package/src/llm/anthropic/types.ts +3 -0
  53. package/src/llm/anthropic/utils/message_inputs.ts +60 -3
  54. package/src/llm/anthropic/utils/message_outputs.ts +10 -2
  55. package/src/llm/anthropic/utils/stream_events.ts +471 -0
  56. package/src/tools/search/crw-scraper.ts +244 -0
  57. package/src/tools/search/crw-search.ts +167 -0
  58. package/src/tools/search/crw.test.ts +836 -0
  59. package/src/tools/search/search.ts +7 -1
  60. package/src/tools/search/tool.ts +23 -3
  61. package/src/tools/search/types.ts +103 -3
@@ -7,14 +7,11 @@
7
7
  //
8
8
  // Applicability to our fork:
9
9
  // Our `CustomAnthropic` (imported here as `ChatAnthropic` from `@/llm/anthropic`)
10
- // extends upstream `ChatAnthropicMessages` (1.5.1). It overrides the LEGACY
11
- // `_streamResponseChunks` path (used by `.stream()` / `.invoke()`), but does NOT
12
- // override `_streamChatModelEvents`. That native method is therefore inherited
13
- // verbatim: it calls `invocationParams` -> `_convertMessagesToAnthropicPayload`
14
- // -> `createStreamWithRetry` -> `convertAnthropicStream`. Our `createStreamWithRetry`
15
- // override only strips unsupported assistant prefill before delegating to super, so
16
- // mocking it (as upstream's `MockStreamChatAnthropic` does) drives the real native
17
- // conversion path. A probe confirmed the lifecycle/sub-streams resolve correctly.
10
+ // extends upstream `ChatAnthropicMessages` (1.5.1). It overrides both the legacy
11
+ // `_streamResponseChunks` path and `_streamChatModelEvents`, whose request setup
12
+ // mirrors upstream while using the local cumulative-usage converter. Mocking
13
+ // `createStreamWithRetry` (as upstream's `MockStreamChatAnthropic` does) drives
14
+ // the real native conversion path without making API calls.
18
15
  //
19
16
  // Adaptation:
20
17
  // - vitest -> jest (`@jest/globals`; `vi.*` -> `jest.*`).
@@ -38,6 +35,7 @@ import type {
38
35
  AnthropicStreamingMessageCreateParams,
39
36
  } from './types';
40
37
  import type { CustomAnthropicCallOptions } from './index';
38
+ import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
41
39
  import { CustomAnthropic as ChatAnthropic } from './index';
42
40
 
43
41
  // ─── Mock model ─────────────────────────────────────────────────
@@ -272,7 +270,7 @@ function cacheUsageEvents(): unknown[] {
272
270
  stop_sequence: null,
273
271
  usage: {
274
272
  input_tokens: 100,
275
- output_tokens: 0,
273
+ output_tokens: 1,
276
274
  cache_creation_input_tokens: 500,
277
275
  cache_read_input_tokens: 200,
278
276
  },
@@ -298,6 +296,302 @@ function cacheUsageEvents(): unknown[] {
298
296
  ];
299
297
  }
300
298
 
299
+ function lateUsageEvents(): unknown[] {
300
+ return [
301
+ {
302
+ type: 'message_start',
303
+ message: {
304
+ id: 'msg_05MNO',
305
+ type: 'message',
306
+ role: 'assistant',
307
+ content: [],
308
+ model: 'claude-sonnet-4-20250514',
309
+ stop_reason: null,
310
+ stop_sequence: null,
311
+ usage: {
312
+ input_tokens: 0,
313
+ output_tokens: 7,
314
+ cache_creation_input_tokens: 0,
315
+ cache_read_input_tokens: 0,
316
+ },
317
+ },
318
+ },
319
+ {
320
+ type: 'message_delta',
321
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
322
+ usage: {
323
+ input_tokens: 100,
324
+ output_tokens: 20,
325
+ cache_creation_input_tokens: 500,
326
+ cache_read_input_tokens: 200,
327
+ },
328
+ },
329
+ {
330
+ type: 'message_delta',
331
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
332
+ usage: {
333
+ input_tokens: 100,
334
+ output_tokens: 42,
335
+ cache_creation_input_tokens: 500,
336
+ cache_read_input_tokens: 200,
337
+ },
338
+ },
339
+ { type: 'message_stop' },
340
+ ];
341
+ }
342
+
343
+ function webSearchResultEvents(): unknown[] {
344
+ return [
345
+ {
346
+ type: 'message_start',
347
+ message: {
348
+ id: 'msg_06PQR',
349
+ type: 'message',
350
+ role: 'assistant',
351
+ content: [],
352
+ model: 'claude-sonnet-4-20250514',
353
+ stop_reason: null,
354
+ stop_sequence: null,
355
+ usage: { input_tokens: 25, output_tokens: 0 },
356
+ },
357
+ },
358
+ {
359
+ type: 'content_block_start',
360
+ index: 0,
361
+ content_block: {
362
+ type: 'web_search_tool_result',
363
+ tool_use_id: 'srvtoolu_01WEB',
364
+ content: [
365
+ {
366
+ type: 'web_search_result',
367
+ url: 'https://example.com',
368
+ title: 'Example',
369
+ encrypted_content: 'encrypted',
370
+ page_age: null,
371
+ },
372
+ ],
373
+ caller: { type: 'direct' },
374
+ },
375
+ },
376
+ { type: 'content_block_stop', index: 0 },
377
+ {
378
+ type: 'message_delta',
379
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
380
+ usage: { output_tokens: 1 },
381
+ },
382
+ { type: 'message_stop' },
383
+ ];
384
+ }
385
+
386
+ function serverToolUseEvents(): unknown[] {
387
+ return [
388
+ {
389
+ type: 'message_start',
390
+ message: {
391
+ id: 'msg_07STU',
392
+ type: 'message',
393
+ role: 'assistant',
394
+ content: [],
395
+ model: 'claude-sonnet-4-20250514',
396
+ stop_reason: null,
397
+ stop_sequence: null,
398
+ usage: { input_tokens: 25, output_tokens: 0 },
399
+ },
400
+ },
401
+ {
402
+ type: 'content_block_start',
403
+ index: 0,
404
+ content_block: {
405
+ type: 'server_tool_use',
406
+ id: 'srvtoolu_01SERVER',
407
+ name: 'web_search',
408
+ input: {},
409
+ },
410
+ },
411
+ {
412
+ type: 'content_block_delta',
413
+ index: 0,
414
+ delta: {
415
+ type: 'input_json_delta',
416
+ partial_json: '{"query":"weather"}',
417
+ },
418
+ },
419
+ { type: 'content_block_stop', index: 0 },
420
+ {
421
+ type: 'message_delta',
422
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
423
+ usage: { output_tokens: 8 },
424
+ },
425
+ { type: 'message_stop' },
426
+ ];
427
+ }
428
+
429
+ function citationEvents(): unknown[] {
430
+ return [
431
+ {
432
+ type: 'message_start',
433
+ message: {
434
+ id: 'msg_08VWX',
435
+ type: 'message',
436
+ role: 'assistant',
437
+ content: [],
438
+ model: 'claude-sonnet-4-20250514',
439
+ stop_reason: null,
440
+ stop_sequence: null,
441
+ usage: { input_tokens: 25, output_tokens: 0 },
442
+ },
443
+ },
444
+ {
445
+ type: 'content_block_start',
446
+ index: 0,
447
+ content_block: { type: 'text', text: '', citations: [] },
448
+ },
449
+ {
450
+ type: 'content_block_delta',
451
+ index: 0,
452
+ delta: {
453
+ type: 'citations_delta',
454
+ citation: {
455
+ type: 'web_search_result_location',
456
+ cited_text: 'Example source',
457
+ encrypted_index: 'encrypted-index',
458
+ title: 'Example',
459
+ url: 'https://example.com',
460
+ },
461
+ },
462
+ },
463
+ {
464
+ type: 'content_block_delta',
465
+ index: 0,
466
+ delta: { type: 'text_delta', text: 'Cited answer.' },
467
+ },
468
+ { type: 'content_block_stop', index: 0 },
469
+ {
470
+ type: 'message_delta',
471
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
472
+ usage: { output_tokens: 5 },
473
+ },
474
+ { type: 'message_stop' },
475
+ ];
476
+ }
477
+
478
+ function compactionEvents(): unknown[] {
479
+ return [
480
+ {
481
+ type: 'message_start',
482
+ message: {
483
+ id: 'msg_09YZA',
484
+ type: 'message',
485
+ role: 'assistant',
486
+ content: [],
487
+ model: 'claude-sonnet-4-20250514',
488
+ stop_reason: null,
489
+ stop_sequence: null,
490
+ usage: { input_tokens: 25, output_tokens: 0 },
491
+ },
492
+ },
493
+ {
494
+ type: 'content_block_start',
495
+ index: 0,
496
+ content_block: {
497
+ type: 'compaction',
498
+ content: null,
499
+ encrypted_content: null,
500
+ },
501
+ },
502
+ {
503
+ type: 'content_block_delta',
504
+ index: 0,
505
+ delta: {
506
+ type: 'compaction_delta',
507
+ content: 'Summary of the earlier conversation.',
508
+ encrypted_content: 'encrypted-summary',
509
+ },
510
+ },
511
+ { type: 'content_block_stop', index: 0 },
512
+ {
513
+ type: 'message_delta',
514
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
515
+ usage: { output_tokens: 5 },
516
+ },
517
+ { type: 'message_stop' },
518
+ ];
519
+ }
520
+
521
+ function redactedThinkingEvents(): unknown[] {
522
+ return [
523
+ {
524
+ type: 'message_start',
525
+ message: {
526
+ id: 'msg_10BCD',
527
+ type: 'message',
528
+ role: 'assistant',
529
+ content: [],
530
+ model: 'claude-sonnet-4-20250514',
531
+ stop_reason: null,
532
+ stop_sequence: null,
533
+ usage: { input_tokens: 25, output_tokens: 0 },
534
+ },
535
+ },
536
+ {
537
+ type: 'content_block_start',
538
+ index: 0,
539
+ content_block: {
540
+ type: 'redacted_thinking',
541
+ data: 'encrypted-thinking',
542
+ },
543
+ },
544
+ { type: 'content_block_stop', index: 0 },
545
+ {
546
+ type: 'message_delta',
547
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
548
+ usage: { output_tokens: 5 },
549
+ },
550
+ { type: 'message_stop' },
551
+ ];
552
+ }
553
+
554
+ function contextWindowExceededEvents(): unknown[] {
555
+ return [
556
+ ...textOnlyEvents().slice(0, -2),
557
+ {
558
+ type: 'message_delta',
559
+ delta: {
560
+ stop_reason: 'model_context_window_exceeded',
561
+ stop_sequence: null,
562
+ },
563
+ usage: { output_tokens: 2 },
564
+ },
565
+ { type: 'message_stop' },
566
+ ];
567
+ }
568
+
569
+ function refusalEvents(): unknown[] {
570
+ return [
571
+ ...textOnlyEvents().slice(0, -2),
572
+ {
573
+ type: 'message_delta',
574
+ delta: { stop_reason: 'refusal', stop_sequence: null },
575
+ usage: { output_tokens: 2 },
576
+ },
577
+ { type: 'message_stop' },
578
+ ];
579
+ }
580
+
581
+ function streamErrorEvents(): unknown[] {
582
+ return [
583
+ textOnlyEvents()[0],
584
+ {
585
+ type: 'error',
586
+ error: {
587
+ type: 'overloaded_error',
588
+ message: 'Anthropic stream overloaded',
589
+ },
590
+ request_id: 'req_01ERROR',
591
+ },
592
+ ];
593
+ }
594
+
301
595
  // ─── Tests ───────────────────────────────────────────────────────
302
596
 
303
597
  describe('CustomAnthropic._streamChatModelEvents (inherited native)', () => {
@@ -572,6 +866,39 @@ describe('CustomAnthropic._streamChatModelEvents (inherited native)', () => {
572
866
  expect(finish.usage.output_tokens).toBe(3);
573
867
  });
574
868
 
869
+ test('usage events preserve cumulative message_delta totals', async () => {
870
+ const model = new MockStreamChatAnthropic(lateUsageEvents());
871
+ const events = await collectEvents(model, {
872
+ streamUsage: true,
873
+ } as BaseChatModelCallOptions);
874
+
875
+ const usageEvents = events.filter((event) => event.event === 'usage');
876
+ const lastUsage = usageEvents[usageEvents.length - 1] as {
877
+ usage: {
878
+ input_tokens: number;
879
+ output_tokens: number;
880
+ total_tokens: number;
881
+ input_token_details: {
882
+ cache_creation: number;
883
+ cache_read: number;
884
+ };
885
+ };
886
+ };
887
+ const expectedUsage = {
888
+ input_tokens: 800,
889
+ output_tokens: 42,
890
+ total_tokens: 842,
891
+ input_token_details: {
892
+ cache_creation: 500,
893
+ cache_read: 200,
894
+ },
895
+ };
896
+ expect(lastUsage.usage).toEqual(expectedUsage);
897
+ expect(
898
+ events.find((event) => event.event === 'message-finish')
899
+ ).toMatchObject({ usage: expectedUsage });
900
+ });
901
+
575
902
  test('no usage events when streamUsage is false', async () => {
576
903
  const model = new MockStreamChatAnthropic(textOnlyEvents());
577
904
  model.streamUsage = false;
@@ -651,6 +978,9 @@ describe('CustomAnthropic._streamChatModelEvents (inherited native)', () => {
651
978
 
652
979
  expect(message.id).toBe('msg_03GHI');
653
980
  expect(message._getType()).toBe('ai');
981
+ expect(message.response_metadata).toMatchObject({
982
+ model_provider: 'anthropic',
983
+ });
654
984
 
655
985
  const content = message.content as Array<{
656
986
  type: string;
@@ -670,6 +1000,153 @@ describe('CustomAnthropic._streamChatModelEvents (inherited native)', () => {
670
1000
  expect(message.tool_calls?.[0]?.name).toBe('web_search');
671
1001
  });
672
1002
 
1003
+ test('output preserves client tool calls for follow-up turns', async () => {
1004
+ const model = new MockStreamChatAnthropic(toolCallEvents());
1005
+ const stream = new ChatModelStream(streamEvents(model));
1006
+ const message = await stream.output;
1007
+
1008
+ const payload = _convertMessagesToAnthropicPayload([message]);
1009
+ expect(payload.messages[0]?.content).toEqual([
1010
+ { type: 'text', text: 'Let me search.' },
1011
+ {
1012
+ type: 'tool_use',
1013
+ id: 'toolu_01ABC',
1014
+ name: 'web_search',
1015
+ input: { query: 'weather' },
1016
+ },
1017
+ ]);
1018
+ });
1019
+
1020
+ test('output preserves signed thinking for follow-up turns', async () => {
1021
+ const model = new MockStreamChatAnthropic(thinkingPlusTextEvents());
1022
+ const stream = new ChatModelStream(streamEvents(model));
1023
+ const message = await stream.output;
1024
+
1025
+ const payload = _convertMessagesToAnthropicPayload([message]);
1026
+ expect(payload.messages[0]?.content).toEqual([
1027
+ {
1028
+ type: 'thinking',
1029
+ thinking: 'Let me reason...',
1030
+ signature: 'sig_abc',
1031
+ },
1032
+ { type: 'text', text: 'The answer is 42.' },
1033
+ ]);
1034
+ });
1035
+
1036
+ test('output preserves citations for follow-up turns', async () => {
1037
+ const model = new MockStreamChatAnthropic(citationEvents());
1038
+ const stream = new ChatModelStream(streamEvents(model));
1039
+ const message = await stream.output;
1040
+ const citation = {
1041
+ type: 'web_search_result_location',
1042
+ cited_text: 'Example source',
1043
+ encrypted_index: 'encrypted-index',
1044
+ title: 'Example',
1045
+ url: 'https://example.com',
1046
+ };
1047
+
1048
+ expect(message.content).toEqual([
1049
+ { type: 'text', text: 'Cited answer.', citations: [citation] },
1050
+ ]);
1051
+ const payload = _convertMessagesToAnthropicPayload([message]);
1052
+ expect(payload.messages[0]?.content).toEqual([
1053
+ { type: 'text', text: 'Cited answer.', citations: [citation] },
1054
+ ]);
1055
+ });
1056
+
1057
+ test('output preserves compaction blocks for follow-up turns', async () => {
1058
+ const model = new MockStreamChatAnthropic(compactionEvents());
1059
+ const stream = new ChatModelStream(streamEvents(model));
1060
+ const message = await stream.output;
1061
+
1062
+ expect(message.content).toEqual([
1063
+ {
1064
+ type: 'compaction',
1065
+ content: 'Summary of the earlier conversation.',
1066
+ encrypted_content: 'encrypted-summary',
1067
+ },
1068
+ ]);
1069
+ const payload = _convertMessagesToAnthropicPayload([message]);
1070
+ expect(payload.messages[0]?.content).toEqual(message.content);
1071
+ });
1072
+
1073
+ test('output preserves redacted thinking for follow-up turns', async () => {
1074
+ const model = new MockStreamChatAnthropic(redactedThinkingEvents());
1075
+ const stream = new ChatModelStream(streamEvents(model));
1076
+ const message = await stream.output;
1077
+
1078
+ expect(message.content).toEqual([
1079
+ { type: 'redacted_thinking', data: 'encrypted-thinking' },
1080
+ ]);
1081
+ const payload = _convertMessagesToAnthropicPayload([message]);
1082
+ expect(payload.messages[0]?.content).toEqual(message.content);
1083
+ });
1084
+
1085
+ test('output preserves web search results for follow-up turns', async () => {
1086
+ const model = new MockStreamChatAnthropic(webSearchResultEvents());
1087
+ const stream = new ChatModelStream(streamEvents(model));
1088
+ const message = await stream.output;
1089
+
1090
+ expect(message.content).toEqual([
1091
+ {
1092
+ type: 'web_search_tool_result',
1093
+ tool_use_id: 'srvtoolu_01WEB',
1094
+ content: [
1095
+ {
1096
+ type: 'web_search_result',
1097
+ url: 'https://example.com',
1098
+ title: 'Example',
1099
+ encrypted_content: 'encrypted',
1100
+ page_age: null,
1101
+ },
1102
+ ],
1103
+ caller: { type: 'direct' },
1104
+ },
1105
+ ]);
1106
+
1107
+ const payload = _convertMessagesToAnthropicPayload([message]);
1108
+ expect(payload.messages[0]?.content).toEqual([
1109
+ {
1110
+ type: 'web_search_tool_result',
1111
+ tool_use_id: 'srvtoolu_01WEB',
1112
+ content: [
1113
+ {
1114
+ type: 'web_search_result',
1115
+ url: 'https://example.com',
1116
+ title: 'Example',
1117
+ encrypted_content: 'encrypted',
1118
+ page_age: null,
1119
+ },
1120
+ ],
1121
+ },
1122
+ ]);
1123
+ });
1124
+
1125
+ test('output preserves server tool arguments for follow-up turns', async () => {
1126
+ const model = new MockStreamChatAnthropic(serverToolUseEvents());
1127
+ const stream = new ChatModelStream(streamEvents(model));
1128
+ const message = await stream.output;
1129
+
1130
+ expect(message.content).toEqual([
1131
+ {
1132
+ type: 'server_tool_call',
1133
+ id: 'srvtoolu_01SERVER',
1134
+ name: 'web_search',
1135
+ args: { query: 'weather' },
1136
+ },
1137
+ ]);
1138
+
1139
+ const payload = _convertMessagesToAnthropicPayload([message]);
1140
+ expect(payload.messages[0]?.content).toEqual([
1141
+ {
1142
+ type: 'server_tool_use',
1143
+ id: 'srvtoolu_01SERVER',
1144
+ name: 'web_search',
1145
+ input: { query: 'weather' },
1146
+ },
1147
+ ]);
1148
+ });
1149
+
673
1150
  test('usage sub-stream works end-to-end', async () => {
674
1151
  const model = new MockStreamChatAnthropic(cacheUsageEvents());
675
1152
  const stream = new ChatModelStream(
@@ -718,6 +1195,33 @@ describe('CustomAnthropic._streamChatModelEvents (inherited native)', () => {
718
1195
  });
719
1196
  });
720
1197
 
1198
+ describe('failure and finish semantics', () => {
1199
+ test('throws Anthropic errors emitted after the stream starts', async () => {
1200
+ const model = new MockStreamChatAnthropic(streamErrorEvents());
1201
+ await expect(collectEvents(model)).rejects.toThrow(
1202
+ 'Anthropic stream overloaded'
1203
+ );
1204
+ });
1205
+
1206
+ test('maps model context exhaustion to a length finish reason', async () => {
1207
+ const model = new MockStreamChatAnthropic(contextWindowExceededEvents());
1208
+ const events = await collectEvents(model);
1209
+
1210
+ expect(events.find((event) => event.event === 'message-finish')).toEqual(
1211
+ expect.objectContaining({ reason: 'length' })
1212
+ );
1213
+ });
1214
+
1215
+ test('maps Anthropic refusals to a content-filter finish reason', async () => {
1216
+ const model = new MockStreamChatAnthropic(refusalEvents());
1217
+ const events = await collectEvents(model);
1218
+
1219
+ expect(events.find((event) => event.event === 'message-finish')).toEqual(
1220
+ expect.objectContaining({ reason: 'content_filter' })
1221
+ );
1222
+ });
1223
+ });
1224
+
721
1225
  // Re-expressed from upstream's `streaming events` describe, which used vitest
722
1226
  // custom matchers (`toHaveStreamText` / `toHaveStreamReasoning` /
723
1227
  // `toHaveStreamToolCalls`) that do not exist in jest. The matchers wrap the