@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.
- package/dist/cjs/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +342 -109
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +64 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +19 -2
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +218 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +54 -41
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +349 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +65 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +7 -7
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +218 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +54 -41
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +27 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/graphs/Graph.ts +698 -200
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
- package/src/llm/bedrock/utils/message_inputs.ts +86 -13
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +196 -2
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +363 -91
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/prune.ts +996 -183
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.ts +23 -15
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +147 -68
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/stream.ts +1 -1
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
package/src/utils/tokens.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Tokenizer } from 'ai-tokenizer';
|
|
2
|
-
import
|
|
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 (
|
|
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 =
|
|
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
|
|
396
|
-
|
|
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(
|
|
412
|
-
|
|
413
|
-
|
|
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?: {
|
|
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
|
|
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
|
|
511
|
-
item.type ===
|
|
512
|
-
|
|
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
|
|
522
|
-
item.type === '
|
|
523
|
-
|
|
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,
|
|
969
|
+
estimateDocumentBlockTokens(item, encoding, countText) *
|
|
527
970
|
IMAGE_TOKEN_SAFETY_MARGIN
|
|
528
971
|
);
|
|
529
972
|
continue;
|
|
530
973
|
}
|
|
531
974
|
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
975
|
+
if (isAtomicToolContentBlock(item) && item.type === 'media') {
|
|
976
|
+
const staticMediaTokens = estimateStaticMediaBlockTokens(
|
|
977
|
+
item,
|
|
978
|
+
encoding,
|
|
979
|
+
countText
|
|
535
980
|
);
|
|
536
|
-
|
|
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 +=
|
|
1002
|
+
numTokens += countText(toolName);
|
|
543
1003
|
}
|
|
544
1004
|
const args = item.tool_call.args;
|
|
545
|
-
if (
|
|
546
|
-
numTokens +=
|
|
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 (
|
|
550
|
-
|
|
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 +=
|
|
1027
|
+
numTokens += countText(value);
|
|
564
1028
|
} else if (typeof value === 'number') {
|
|
565
|
-
numTokens +=
|
|
1029
|
+
numTokens += countText(value.toString());
|
|
566
1030
|
} else if (typeof value === 'boolean') {
|
|
567
|
-
numTokens +=
|
|
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
|
-
|
|
573
|
-
|
|
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
|
/**
|