@librechat/agents 3.3.3 → 3.3.4

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 (156) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +342 -109
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  10. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/invoke.cjs +64 -3
  14. package/dist/cjs/llm/invoke.cjs.map +1 -1
  15. package/dist/cjs/llm/openai/index.cjs +41 -5
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  18. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  20. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  21. package/dist/cjs/main.cjs +19 -2
  22. package/dist/cjs/messages/cache.cjs +54 -33
  23. package/dist/cjs/messages/cache.cjs.map +1 -1
  24. package/dist/cjs/messages/contextPruning.cjs +17 -43
  25. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  26. package/dist/cjs/messages/core.cjs +315 -23
  27. package/dist/cjs/messages/core.cjs.map +1 -1
  28. package/dist/cjs/messages/format.cjs +218 -60
  29. package/dist/cjs/messages/format.cjs.map +1 -1
  30. package/dist/cjs/messages/index.cjs +1 -1
  31. package/dist/cjs/messages/prune.cjs +503 -127
  32. package/dist/cjs/messages/prune.cjs.map +1 -1
  33. package/dist/cjs/stream.cjs +7 -3
  34. package/dist/cjs/stream.cjs.map +1 -1
  35. package/dist/cjs/summarization/node.cjs +28 -14
  36. package/dist/cjs/summarization/node.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +54 -41
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  40. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  41. package/dist/cjs/utils/index.cjs +1 -1
  42. package/dist/cjs/utils/llm.cjs +1 -1
  43. package/dist/cjs/utils/llm.cjs.map +1 -1
  44. package/dist/cjs/utils/tokens.cjs +307 -20
  45. package/dist/cjs/utils/tokens.cjs.map +1 -1
  46. package/dist/cjs/utils/toolContent.cjs +1514 -0
  47. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  48. package/dist/esm/agents/AgentContext.mjs +2 -1
  49. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  50. package/dist/esm/graphs/Graph.mjs +349 -116
  51. package/dist/esm/graphs/Graph.mjs.map +1 -1
  52. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  54. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  55. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  56. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  57. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  58. package/dist/esm/llm/google/utils/common.mjs +19 -7
  59. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  60. package/dist/esm/llm/invoke.mjs +65 -5
  61. package/dist/esm/llm/invoke.mjs.map +1 -1
  62. package/dist/esm/llm/openai/index.mjs +41 -5
  63. package/dist/esm/llm/openai/index.mjs.map +1 -1
  64. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  65. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  66. package/dist/esm/llm/openrouter/index.mjs +4 -1
  67. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  68. package/dist/esm/main.mjs +7 -7
  69. package/dist/esm/messages/cache.mjs +55 -34
  70. package/dist/esm/messages/cache.mjs.map +1 -1
  71. package/dist/esm/messages/contextPruning.mjs +17 -43
  72. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  73. package/dist/esm/messages/core.mjs +305 -24
  74. package/dist/esm/messages/core.mjs.map +1 -1
  75. package/dist/esm/messages/format.mjs +218 -61
  76. package/dist/esm/messages/format.mjs.map +1 -1
  77. package/dist/esm/messages/index.mjs +1 -1
  78. package/dist/esm/messages/prune.mjs +500 -128
  79. package/dist/esm/messages/prune.mjs.map +1 -1
  80. package/dist/esm/stream.mjs +7 -3
  81. package/dist/esm/stream.mjs.map +1 -1
  82. package/dist/esm/summarization/node.mjs +28 -14
  83. package/dist/esm/summarization/node.mjs.map +1 -1
  84. package/dist/esm/tools/ToolNode.mjs +54 -41
  85. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  86. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  87. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  88. package/dist/esm/utils/index.mjs +1 -1
  89. package/dist/esm/utils/llm.mjs +1 -1
  90. package/dist/esm/utils/llm.mjs.map +1 -1
  91. package/dist/esm/utils/tokens.mjs +307 -21
  92. package/dist/esm/utils/tokens.mjs.map +1 -1
  93. package/dist/esm/utils/toolContent.mjs +1503 -0
  94. package/dist/esm/utils/toolContent.mjs.map +1 -0
  95. package/dist/types/graphs/Graph.d.ts +1 -1
  96. package/dist/types/llm/invoke.d.ts +27 -1
  97. package/dist/types/llm/openai/index.d.ts +4 -0
  98. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  99. package/dist/types/llm/openrouter/index.d.ts +5 -0
  100. package/dist/types/messages/contextPruning.d.ts +2 -1
  101. package/dist/types/messages/core.d.ts +36 -0
  102. package/dist/types/messages/format.d.ts +6 -0
  103. package/dist/types/messages/prune.d.ts +16 -12
  104. package/dist/types/types/stream.d.ts +1 -1
  105. package/dist/types/utils/tokens.d.ts +7 -0
  106. package/dist/types/utils/toolContent.d.ts +107 -0
  107. package/package.json +1 -1
  108. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  109. package/src/agents/AgentContext.ts +1 -0
  110. package/src/graphs/Graph.ts +698 -200
  111. package/src/graphs/MultiAgentGraph.ts +6 -1
  112. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  113. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  114. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  115. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  116. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  117. package/src/llm/google/utils/common.test.ts +75 -9
  118. package/src/llm/google/utils/common.ts +34 -3
  119. package/src/llm/invoke.test.ts +488 -0
  120. package/src/llm/invoke.ts +196 -2
  121. package/src/llm/openai/index.ts +106 -6
  122. package/src/llm/openai/utils/index.ts +109 -53
  123. package/src/llm/openai/utils/messages.test.ts +330 -1
  124. package/src/llm/openrouter/index.ts +12 -2
  125. package/src/llm/openrouter/reasoning.test.ts +314 -0
  126. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  127. package/src/messages/cache.tail.test.ts +193 -0
  128. package/src/messages/cache.test.ts +113 -0
  129. package/src/messages/cache.ts +92 -60
  130. package/src/messages/contextPruning.test.ts +184 -0
  131. package/src/messages/contextPruning.ts +49 -42
  132. package/src/messages/core.ts +653 -40
  133. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  134. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  135. package/src/messages/format.ts +363 -91
  136. package/src/messages/formatAgentMessages.test.ts +604 -14
  137. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  138. package/src/messages/prune.ts +996 -183
  139. package/src/specs/prune.test.ts +1083 -6
  140. package/src/specs/summarization-unit.test.ts +105 -0
  141. package/src/specs/tokens.test.ts +609 -32
  142. package/src/stream.ts +23 -15
  143. package/src/summarization/__tests__/node.test.ts +77 -0
  144. package/src/summarization/node.ts +46 -18
  145. package/src/tools/ToolNode.ts +147 -68
  146. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  147. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  148. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  149. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  150. package/src/tools/toolOutputReferences.ts +6 -0
  151. package/src/types/stream.ts +1 -1
  152. package/src/utils/llm.test.ts +18 -0
  153. package/src/utils/llm.ts +4 -1
  154. package/src/utils/tokens.ts +586 -31
  155. package/src/utils/toolContent.test.ts +1432 -0
  156. package/src/utils/toolContent.ts +2707 -0
@@ -1,5 +1,11 @@
1
1
  import { Tokenizer } from 'ai-tokenizer';
2
- import type { BaseMessage } from '@langchain/core/messages';
2
+ import { isProxy } from 'node:util/types';
3
+ import type { AIMessage, BaseMessage } from '@langchain/core/messages';
4
+ import {
5
+ isComputerCallOutputContent,
6
+ isAtomicToolContentBlock,
7
+ serializeStructuredValueBounded,
8
+ } from './toolContent';
3
9
  import { ContentTypes } from '@/common/enum';
4
10
 
5
11
  export type EncodingName = 'o200k_base' | 'claude';
@@ -59,6 +65,15 @@ const TIMED_MEDIA_TYPES = new Set([
59
65
  'video_url',
60
66
  'input_audio',
61
67
  ]);
68
+ /**
69
+ * Structured content is tokenized exactly up to this size. Larger values are
70
+ * traversed by the bounded serializer and priced conservatively for the omitted
71
+ * suffix, avoiding a full JSON string allocation in the context guard.
72
+ */
73
+ const MAX_STRUCTURED_TOKENIZATION_CHARS = 200_000;
74
+ /** One UTF-16 character can encode to several tokenizer pieces. */
75
+ const MAX_TOKENS_PER_OMITTED_STRUCTURED_CHAR = 4;
76
+ const MAX_STRUCTURED_SAFETY_INSPECTION_WORK = 10_000;
62
77
 
63
78
  /**
64
79
  * Extracts image dimensions from the first bytes of a base64-encoded
@@ -168,11 +183,20 @@ export function estimateImageBlockTokens(
168
183
  ): number {
169
184
  let base64Data: string | undefined;
170
185
 
171
- if (block.type === ContentTypes.IMAGE_URL || block.type === 'image_url') {
186
+ if (
187
+ block.type === ContentTypes.IMAGE_URL ||
188
+ block.type === 'image_url' ||
189
+ block.type === 'computer_screenshot' ||
190
+ block.type === 'input_image'
191
+ ) {
172
192
  const imageUrl = block.image_url as string | { url?: string } | undefined;
173
193
  const url = typeof imageUrl === 'string' ? imageUrl : imageUrl?.url;
174
194
  if (typeof url === 'string' && url.startsWith('data:')) {
175
195
  base64Data = url;
196
+ } else if (typeof block.file_id === 'string' && block.file_id !== '') {
197
+ return block.detail === 'low'
198
+ ? OPENAI_IMAGE_LOW_TOKENS
199
+ : ANTHROPIC_IMAGE_MIN_TOKENS;
176
200
  } else {
177
201
  return ANTHROPIC_IMAGE_MIN_TOKENS;
178
202
  }
@@ -385,15 +409,16 @@ function mediaBlockByteLength(block: Record<string, unknown>): number {
385
409
  function timedMediaKind(
386
410
  block: Record<string, unknown>
387
411
  ): 'video' | 'audio' | null {
388
- const type = typeof block.type === 'string' ? block.type : '';
412
+ const type =
413
+ typeof block.type === 'string' ? normalizeMediaMimeType(block.type) : '';
389
414
  if (type === 'input_audio' || type === 'audio') {
390
415
  return 'audio';
391
416
  }
392
417
  if (type === 'video_url' || type === 'video') {
393
418
  return 'video';
394
419
  }
395
- const mime =
396
- type === 'media' && typeof block.mimeType === 'string' ? block.mimeType : type;
420
+ const mediaMime = getMediaMimeType(block);
421
+ const mime = type === 'media' && mediaMime != null ? mediaMime : type;
397
422
  if (mime.startsWith('audio/')) {
398
423
  return 'audio';
399
424
  }
@@ -403,17 +428,145 @@ function timedMediaKind(
403
428
  return null;
404
429
  }
405
430
 
431
+ function getMediaMimeType(block: Record<string, unknown>): string | undefined {
432
+ if (typeof block.mimeType === 'string') {
433
+ return normalizeMediaMimeType(block.mimeType);
434
+ }
435
+ if (typeof block.mime_type === 'string') {
436
+ return normalizeMediaMimeType(block.mime_type);
437
+ }
438
+ return undefined;
439
+ }
440
+
441
+ function normalizeMediaMimeType(mimeType: string): string {
442
+ return mimeType.split(';')[0].trim().toLowerCase();
443
+ }
444
+
406
445
  /** Whether a content-block `type` can carry timed media — the fixed set plus the
407
446
  * Google MIME-as-type shape (`audio/*` / `video/*`). Gates callers before they
408
447
  * hand the block to {@link estimateTimedMediaBlockTokens}. */
409
448
  function isTimedMediaType(type: string): boolean {
449
+ const normalizedType = normalizeMediaMimeType(type);
410
450
  return (
411
- TIMED_MEDIA_TYPES.has(type) ||
412
- type.startsWith('audio/') ||
413
- type.startsWith('video/')
451
+ TIMED_MEDIA_TYPES.has(normalizedType) ||
452
+ normalizedType.startsWith('audio/') ||
453
+ normalizedType.startsWith('video/')
414
454
  );
415
455
  }
416
456
 
457
+ /** Conservatively prices unknown inline media without materializing JSON. */
458
+ function estimateUnknownMediaBlockTokens(data: unknown): number {
459
+ if (typeof data === 'string') {
460
+ if (base64ByteLength(data) <= 0) {
461
+ return URL_DOCUMENT_FALLBACK_TOKENS;
462
+ }
463
+ return Math.max(URL_DOCUMENT_FALLBACK_TOKENS, data.length);
464
+ }
465
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
466
+ return Math.max(
467
+ URL_DOCUMENT_FALLBACK_TOKENS,
468
+ Math.ceil((data.byteLength * 4) / 3)
469
+ );
470
+ }
471
+ return URL_DOCUMENT_FALLBACK_TOKENS;
472
+ }
473
+
474
+ /**
475
+ * Estimates Google `type: "media"` blocks whose MIME describes static content.
476
+ * Timed audio/video continues through {@link estimateTimedMediaBlockTokens}.
477
+ */
478
+ function estimateStaticMediaBlockTokens(
479
+ block: Record<string, unknown>,
480
+ encoding: EncodingName,
481
+ getTokenCount: (text: string) => number
482
+ ): number | undefined {
483
+ const mediaMime = getMediaMimeType(block);
484
+ if (block.type !== 'media' || mediaMime == null) {
485
+ return undefined;
486
+ }
487
+
488
+ const mimeType = mediaMime;
489
+ const nestedMedia =
490
+ block.media != null && typeof block.media === 'object'
491
+ ? (block.media as Record<string, unknown>)
492
+ : undefined;
493
+ const data =
494
+ block.data ?? block.bytes ?? nestedMedia?.data ?? nestedMedia?.bytes;
495
+ const reference =
496
+ block.url ??
497
+ block.fileUri ??
498
+ block.fileId ??
499
+ nestedMedia?.url ??
500
+ nestedMedia?.fileUri ??
501
+ nestedMedia?.fileId;
502
+
503
+ if (mimeType.startsWith('image/')) {
504
+ let encodedImage: string | undefined;
505
+ if (typeof data === 'string') {
506
+ encodedImage = data;
507
+ } else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
508
+ const view =
509
+ data instanceof ArrayBuffer
510
+ ? new Uint8Array(data)
511
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
512
+ encodedImage = Buffer.from(view.subarray(0, 80)).toString('base64');
513
+ }
514
+
515
+ if (encodedImage != null && encodedImage.length > 0) {
516
+ return estimateImageBlockTokens(
517
+ {
518
+ type: 'image',
519
+ source: { type: 'base64', data: encodedImage },
520
+ },
521
+ encoding
522
+ );
523
+ }
524
+ return estimateImageBlockTokens(
525
+ {
526
+ type: 'image_url',
527
+ image_url: { url: typeof reference === 'string' ? reference : '' },
528
+ },
529
+ encoding
530
+ );
531
+ }
532
+
533
+ if (mimeType === 'application/pdf') {
534
+ if (typeof data === 'string' && data.length > 0) {
535
+ const comma = data.startsWith('data:') ? data.indexOf(',') : -1;
536
+ const base64Data = comma >= 0 ? data.slice(comma + 1) : data;
537
+ return estimateDocumentBlockTokens(
538
+ {
539
+ type: 'file',
540
+ source_type: 'base64',
541
+ mime_type: mimeType,
542
+ data: base64Data,
543
+ },
544
+ encoding,
545
+ getTokenCount
546
+ );
547
+ }
548
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
549
+ const estimatedBase64Chars = Math.ceil((data.byteLength * 4) / 3);
550
+ const pdfTokensPerPage =
551
+ encoding === 'claude'
552
+ ? ANTHROPIC_PDF_TOKENS_PER_PAGE
553
+ : OPENAI_PDF_TOKENS_PER_PAGE;
554
+ return (
555
+ Math.max(
556
+ 1,
557
+ Math.ceil(estimatedBase64Chars / BASE64_BYTES_PER_PDF_PAGE)
558
+ ) * pdfTokensPerPage
559
+ );
560
+ }
561
+ return URL_DOCUMENT_FALLBACK_TOKENS;
562
+ }
563
+
564
+ if (mimeType.startsWith('audio/') || mimeType.startsWith('video/')) {
565
+ return undefined;
566
+ }
567
+ return estimateUnknownMediaBlockTokens(data);
568
+ }
569
+
417
570
  /**
418
571
  * Estimates token cost for a timed-media block (video/audio). Handles Google
419
572
  * `{ type: 'media', mimeType, data|url|fileUri }`, OpenRouter
@@ -483,33 +636,322 @@ export function encodingForModel(model: string): EncodingName {
483
636
  return 'o200k_base';
484
637
  }
485
638
 
639
+ type StructuredSafetyInspection = {
640
+ remaining: number;
641
+ };
642
+
643
+ function consumeStructuredSafetyWork(
644
+ inspection: StructuredSafetyInspection
645
+ ): boolean {
646
+ if (inspection.remaining <= 0) {
647
+ return false;
648
+ }
649
+ inspection.remaining--;
650
+ return true;
651
+ }
652
+
653
+ function hasUnsafeToJSON(
654
+ value: object,
655
+ inspection: StructuredSafetyInspection
656
+ ): boolean {
657
+ let current: object | null = value;
658
+ const seen = new Set<object>();
659
+ try {
660
+ while (current != null) {
661
+ if (isProxy(current)) {
662
+ return true;
663
+ }
664
+ if (seen.has(current) || !consumeStructuredSafetyWork(inspection)) {
665
+ return true;
666
+ }
667
+ seen.add(current);
668
+ const descriptor = Object.getOwnPropertyDescriptor(current, 'toJSON');
669
+ if (descriptor != null) {
670
+ return (
671
+ ('value' in descriptor && typeof descriptor.value === 'function') ||
672
+ !('value' in descriptor)
673
+ );
674
+ }
675
+ current = Object.getPrototypeOf(current) as object | null;
676
+ }
677
+ } catch {
678
+ return true;
679
+ }
680
+ return false;
681
+ }
682
+
683
+ const arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor(
684
+ ArrayBuffer.prototype,
685
+ 'byteLength'
686
+ )?.get;
687
+
688
+ /**
689
+ * Tests native binary values without `instanceof`, whose prototype walk can
690
+ * recurse or throw for adversarial proxies.
691
+ */
692
+ function isNativeBinaryValue(value: object): boolean {
693
+ try {
694
+ if (ArrayBuffer.isView(value)) {
695
+ return true;
696
+ }
697
+ } catch {
698
+ return true;
699
+ }
700
+ if (arrayBufferByteLengthGetter == null) {
701
+ return false;
702
+ }
703
+ try {
704
+ arrayBufferByteLengthGetter.call(value);
705
+ return true;
706
+ } catch {
707
+ return false;
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Detects an inherited accessor without reading it. Any proxy/prototype trap or
713
+ * cyclic prototype chain is unsafe rather than observable to the caller.
714
+ */
715
+ function hasUnsafeInheritedProperty(
716
+ value: object,
717
+ key: string,
718
+ inspection: StructuredSafetyInspection
719
+ ): boolean {
720
+ const seen = new Set<object>([value]);
721
+ let current: object | null;
722
+ try {
723
+ current = Object.getPrototypeOf(value) as object | null;
724
+ } catch {
725
+ return true;
726
+ }
727
+
728
+ while (current != null) {
729
+ if (isProxy(current)) {
730
+ return true;
731
+ }
732
+ if (seen.has(current) || !consumeStructuredSafetyWork(inspection)) {
733
+ return true;
734
+ }
735
+ seen.add(current);
736
+ let descriptor: PropertyDescriptor | undefined;
737
+ try {
738
+ descriptor = Object.getOwnPropertyDescriptor(current, key);
739
+ } catch {
740
+ return true;
741
+ }
742
+ if (descriptor != null) {
743
+ return !('value' in descriptor);
744
+ }
745
+ try {
746
+ current = Object.getPrototypeOf(current) as object | null;
747
+ } catch {
748
+ return true;
749
+ }
750
+ }
751
+ return false;
752
+ }
753
+
754
+ /**
755
+ * Detects values whose native JSON representation can differ after local
756
+ * measurement. Accessor properties and `toJSON` hooks are deliberately not
757
+ * invoked; values too large to inspect within bounded work are treated as
758
+ * unsafe so the caller can normalize or reject them conservatively.
759
+ */
760
+ export function hasUnsafeStructuredSerialization(value: unknown): boolean {
761
+ if (typeof value === 'bigint') {
762
+ return true;
763
+ }
764
+ if (value == null || typeof value !== 'object') {
765
+ return false;
766
+ }
767
+ if (isProxy(value)) {
768
+ return true;
769
+ }
770
+ if (isNativeBinaryValue(value)) {
771
+ return true;
772
+ }
773
+
774
+ const stack: object[] = [value];
775
+ const seen = new Set<object>();
776
+ const inspection: StructuredSafetyInspection = {
777
+ remaining: MAX_STRUCTURED_SAFETY_INSPECTION_WORK,
778
+ };
779
+ while (stack.length > 0) {
780
+ const current = stack.pop() as object;
781
+ if (isProxy(current)) {
782
+ return true;
783
+ }
784
+ if (seen.has(current)) {
785
+ return true;
786
+ }
787
+ seen.add(current);
788
+ if (!consumeStructuredSafetyWork(inspection)) {
789
+ return true;
790
+ }
791
+
792
+ try {
793
+ if (hasUnsafeToJSON(current, inspection)) {
794
+ return true;
795
+ }
796
+ for (const key in current) {
797
+ if (!consumeStructuredSafetyWork(inspection)) {
798
+ return true;
799
+ }
800
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
801
+ if (descriptor == null) {
802
+ if (hasUnsafeInheritedProperty(current, key, inspection)) {
803
+ return true;
804
+ }
805
+ continue;
806
+ }
807
+ if (descriptor.enumerable !== true) {
808
+ continue;
809
+ }
810
+ if (!('value' in descriptor)) {
811
+ return true;
812
+ }
813
+ if ('value' in descriptor) {
814
+ if (typeof descriptor.value === 'bigint') {
815
+ return true;
816
+ }
817
+ if (
818
+ descriptor.value != null &&
819
+ typeof descriptor.value === 'object' &&
820
+ isNativeBinaryValue(descriptor.value)
821
+ ) {
822
+ return true;
823
+ }
824
+ if (
825
+ descriptor.value != null &&
826
+ typeof descriptor.value === 'object'
827
+ ) {
828
+ stack.push(descriptor.value);
829
+ }
830
+ }
831
+ }
832
+ } catch {
833
+ return true;
834
+ }
835
+ }
836
+ return false;
837
+ }
838
+
839
+ function getBoundedTextTokenCount(
840
+ value: string,
841
+ getTokenCount: (text: string) => number
842
+ ): number {
843
+ const preview =
844
+ value.length > MAX_STRUCTURED_TOKENIZATION_CHARS
845
+ ? value.slice(0, MAX_STRUCTURED_TOKENIZATION_CHARS)
846
+ : value;
847
+ const previewTokens = getTokenCount(preview);
848
+ const omittedChars = value.length - preview.length;
849
+ if (omittedChars <= 0) {
850
+ return previewTokens;
851
+ }
852
+ return Math.min(
853
+ Number.MAX_SAFE_INTEGER,
854
+ previewTokens + omittedChars * MAX_TOKENS_PER_OMITTED_STRUCTURED_CHAR
855
+ );
856
+ }
857
+
858
+ function getBoundedStructuredTokenCount(
859
+ value: unknown,
860
+ getTokenCount: (text: string) => number
861
+ ): number {
862
+ if (hasUnsafeStructuredSerialization(value)) {
863
+ return Number.MAX_SAFE_INTEGER;
864
+ }
865
+ const serialized = serializeStructuredValueBounded(
866
+ value,
867
+ MAX_STRUCTURED_TOKENIZATION_CHARS
868
+ );
869
+ const previewTokens = getTokenCount(serialized.content);
870
+ if (!serialized.truncated) {
871
+ return previewTokens;
872
+ }
873
+ if (!Number.isSafeInteger(serialized.originalChars)) {
874
+ return Number.MAX_SAFE_INTEGER;
875
+ }
876
+
877
+ const omittedChars = Math.max(
878
+ 0,
879
+ serialized.originalChars - serialized.content.length
880
+ );
881
+ return Math.min(
882
+ Number.MAX_SAFE_INTEGER,
883
+ previewTokens + omittedChars * MAX_TOKENS_PER_OMITTED_STRUCTURED_CHAR
884
+ );
885
+ }
886
+
486
887
  export function getTokenCountForMessage(
487
888
  message: BaseMessage,
488
889
  getTokenCount: (text: string) => number,
489
890
  encoding: EncodingName = 'o200k_base'
490
891
  ): number {
491
892
  const tokensPerMessage = 3;
893
+ const countText = (text: string): number =>
894
+ getBoundedTextTokenCount(text, getTokenCount);
895
+ if (isProxy(message)) {
896
+ return Number.MAX_SAFE_INTEGER;
897
+ }
492
898
 
493
899
  type ContentBlock = Record<string, unknown> & {
494
900
  type?: string;
495
- tool_call?: { name?: string; args?: string; output?: string };
901
+ tool_call?: {
902
+ id?: string;
903
+ name?: string;
904
+ args?: unknown;
905
+ output?: unknown;
906
+ };
496
907
  };
908
+ const representedToolCallIds = new Set<string>();
497
909
 
498
910
  const processValue = (value: unknown): void => {
911
+ if (value != null && typeof value === 'object' && isProxy(value)) {
912
+ numTokens = Number.MAX_SAFE_INTEGER;
913
+ return;
914
+ }
499
915
  if (Array.isArray(value)) {
500
916
  for (const raw of value) {
917
+ if (
918
+ typeof raw === 'string' ||
919
+ typeof raw === 'number' ||
920
+ typeof raw === 'boolean'
921
+ ) {
922
+ processValue(raw);
923
+ continue;
924
+ }
501
925
  const item = raw as ContentBlock | null | undefined;
502
- if (item == null || typeof item.type !== 'string') {
926
+ if (item == null || typeof item !== 'object') {
927
+ continue;
928
+ }
929
+ if (isProxy(item)) {
930
+ numTokens = Number.MAX_SAFE_INTEGER;
931
+ return;
932
+ }
933
+ if (typeof item.type !== 'string') {
934
+ numTokens += getBoundedStructuredTokenCount(item, countText);
503
935
  continue;
504
936
  }
937
+ if (item.type === ContentTypes.TOOL_CALL || item.type === 'tool_use') {
938
+ const inlineId =
939
+ typeof item.id === 'string' ? item.id : item.tool_call?.id;
940
+ if (typeof inlineId === 'string' && inlineId.length > 0) {
941
+ representedToolCallIds.add(inlineId);
942
+ }
943
+ }
505
944
  if (item.type === ContentTypes.ERROR) {
506
945
  continue;
507
946
  }
508
947
 
509
948
  if (
510
- item.type === ContentTypes.IMAGE_URL ||
511
- item.type === 'image_url' ||
512
- item.type === 'image'
949
+ isAtomicToolContentBlock(item) &&
950
+ (item.type === ContentTypes.IMAGE_URL ||
951
+ item.type === 'image_url' ||
952
+ item.type === 'image' ||
953
+ item.type === 'computer_screenshot' ||
954
+ item.type === 'input_image')
513
955
  ) {
514
956
  numTokens += Math.ceil(
515
957
  estimateImageBlockTokens(item, encoding) * IMAGE_TOKEN_SAFETY_MARGIN
@@ -518,59 +960,172 @@ export function getTokenCountForMessage(
518
960
  }
519
961
 
520
962
  if (
521
- item.type === 'document' ||
522
- item.type === 'file' ||
523
- item.type === ContentTypes.IMAGE_FILE
963
+ isAtomicToolContentBlock(item) &&
964
+ (item.type === 'document' ||
965
+ item.type === 'file' ||
966
+ item.type === ContentTypes.IMAGE_FILE)
524
967
  ) {
525
968
  numTokens += Math.ceil(
526
- estimateDocumentBlockTokens(item, encoding, getTokenCount) *
969
+ estimateDocumentBlockTokens(item, encoding, countText) *
527
970
  IMAGE_TOKEN_SAFETY_MARGIN
528
971
  );
529
972
  continue;
530
973
  }
531
974
 
532
- if (isTimedMediaType(item.type)) {
533
- numTokens += Math.ceil(
534
- estimateTimedMediaBlockTokens(item) * IMAGE_TOKEN_SAFETY_MARGIN
975
+ if (isAtomicToolContentBlock(item) && item.type === 'media') {
976
+ const staticMediaTokens = estimateStaticMediaBlockTokens(
977
+ item,
978
+ encoding,
979
+ countText
535
980
  );
536
- continue;
981
+ if (staticMediaTokens != null) {
982
+ numTokens += Math.ceil(
983
+ staticMediaTokens * IMAGE_TOKEN_SAFETY_MARGIN
984
+ );
985
+ continue;
986
+ }
987
+ }
988
+
989
+ if (isAtomicToolContentBlock(item) && isTimedMediaType(item.type)) {
990
+ const timedMediaTokens = estimateTimedMediaBlockTokens(item);
991
+ if (timedMediaTokens > 0) {
992
+ numTokens += Math.ceil(
993
+ timedMediaTokens * IMAGE_TOKEN_SAFETY_MARGIN
994
+ );
995
+ continue;
996
+ }
537
997
  }
538
998
 
539
999
  if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) {
540
1000
  const toolName = item.tool_call.name;
541
1001
  if (typeof toolName === 'string' && toolName.length > 0) {
542
- numTokens += getTokenCount(toolName);
1002
+ numTokens += countText(toolName);
543
1003
  }
544
1004
  const args = item.tool_call.args;
545
- if (typeof args === 'string' && args.length > 0) {
546
- numTokens += getTokenCount(args);
1005
+ if (args != null) {
1006
+ numTokens +=
1007
+ typeof args === 'string'
1008
+ ? countText(args)
1009
+ : getBoundedStructuredTokenCount(args, countText);
547
1010
  }
548
1011
  const output = item.tool_call.output;
549
- if (typeof output === 'string' && output.length > 0) {
550
- numTokens += getTokenCount(output);
1012
+ if (output != null) {
1013
+ processValue(output);
551
1014
  }
552
1015
  continue;
553
1016
  }
554
1017
 
555
1018
  const nestedValue = item[item.type];
556
1019
  if (nestedValue == null) {
1020
+ numTokens += getBoundedStructuredTokenCount(item, countText);
557
1021
  continue;
558
1022
  }
559
1023
 
560
1024
  processValue(nestedValue);
561
1025
  }
562
1026
  } else if (typeof value === 'string') {
563
- numTokens += getTokenCount(value);
1027
+ numTokens += countText(value);
564
1028
  } else if (typeof value === 'number') {
565
- numTokens += getTokenCount(value.toString());
1029
+ numTokens += countText(value.toString());
566
1030
  } else if (typeof value === 'boolean') {
567
- numTokens += getTokenCount(value.toString());
1031
+ numTokens += countText(value.toString());
1032
+ } else if (value != null && typeof value === 'object') {
1033
+ numTokens += getBoundedStructuredTokenCount(value, countText);
568
1034
  }
569
1035
  };
570
1036
 
1037
+ const rawAdditionalKwargs = message.additional_kwargs as unknown;
1038
+ const additionalKwargs =
1039
+ rawAdditionalKwargs != null && typeof rawAdditionalKwargs === 'object'
1040
+ ? rawAdditionalKwargs
1041
+ : undefined;
1042
+ if (additionalKwargs != null && isProxy(additionalKwargs)) {
1043
+ return Number.MAX_SAFE_INTEGER;
1044
+ }
1045
+ let additionalType: PropertyDescriptor | undefined;
1046
+ try {
1047
+ additionalType =
1048
+ additionalKwargs != null
1049
+ ? Object.getOwnPropertyDescriptor(additionalKwargs, 'type')
1050
+ : undefined;
1051
+ } catch {
1052
+ return Number.MAX_SAFE_INTEGER;
1053
+ }
1054
+ if (additionalType != null && !('value' in additionalType)) {
1055
+ return Number.MAX_SAFE_INTEGER;
1056
+ }
1057
+
571
1058
  let numTokens = tokensPerMessage;
572
- processValue(message.content);
573
- return numTokens;
1059
+ const messageType = message.getType();
1060
+ const isComputerCallOutput =
1061
+ messageType === 'tool' &&
1062
+ additionalType?.value === 'computer_call_output' &&
1063
+ isComputerCallOutputContent(message.content);
1064
+ if (isComputerCallOutput && typeof message.content === 'string') {
1065
+ numTokens += Math.ceil(
1066
+ estimateImageBlockTokens(
1067
+ {
1068
+ type: 'computer_screenshot',
1069
+ image_url: message.content,
1070
+ },
1071
+ encoding
1072
+ ) * IMAGE_TOKEN_SAFETY_MARGIN
1073
+ );
1074
+ } else {
1075
+ processValue(message.content);
1076
+ }
1077
+ if (numTokens >= Number.MAX_SAFE_INTEGER) {
1078
+ return Number.MAX_SAFE_INTEGER;
1079
+ }
1080
+ const messageRole = (message as BaseMessage & { role?: unknown }).role;
1081
+ if (messageType === 'ai' || messageRole === 'assistant') {
1082
+ const toolCalls = (message as AIMessage).tool_calls ?? [];
1083
+ if (isProxy(toolCalls)) {
1084
+ return Number.MAX_SAFE_INTEGER;
1085
+ }
1086
+ for (const toolCall of toolCalls) {
1087
+ if (isProxy(toolCall)) {
1088
+ return Number.MAX_SAFE_INTEGER;
1089
+ }
1090
+ if (
1091
+ typeof toolCall.id === 'string' &&
1092
+ representedToolCallIds.has(toolCall.id)
1093
+ ) {
1094
+ continue;
1095
+ }
1096
+ if (typeof toolCall.name === 'string' && toolCall.name.length > 0) {
1097
+ numTokens += countText(toolCall.name);
1098
+ }
1099
+ const args: unknown = toolCall.args;
1100
+ if (args != null) {
1101
+ numTokens +=
1102
+ typeof args === 'string'
1103
+ ? countText(args)
1104
+ : getBoundedStructuredTokenCount(args, countText);
1105
+ }
1106
+ }
1107
+ let legacyFunctionCall: PropertyDescriptor | undefined;
1108
+ try {
1109
+ legacyFunctionCall =
1110
+ additionalKwargs != null
1111
+ ? Object.getOwnPropertyDescriptor(additionalKwargs, 'function_call')
1112
+ : undefined;
1113
+ } catch {
1114
+ return Number.MAX_SAFE_INTEGER;
1115
+ }
1116
+ if (legacyFunctionCall != null) {
1117
+ if (!('value' in legacyFunctionCall)) {
1118
+ return Number.MAX_SAFE_INTEGER;
1119
+ }
1120
+ if (legacyFunctionCall.value != null) {
1121
+ numTokens += getBoundedStructuredTokenCount(
1122
+ legacyFunctionCall.value,
1123
+ countText
1124
+ );
1125
+ }
1126
+ }
1127
+ }
1128
+ return Math.min(Number.MAX_SAFE_INTEGER, numTokens);
574
1129
  }
575
1130
 
576
1131
  /**