@librechat/agents 3.3.12 → 3.4.0

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 (109) hide show
  1. package/dist/cjs/graphs/Graph.cjs +10 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +1 -0
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuseSpanRegistry.cjs +6 -3
  6. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/index.cjs +35 -206
  8. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +121 -241
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/index.cjs +19 -12
  12. package/dist/cjs/llm/google/index.cjs.map +1 -1
  13. package/dist/cjs/llm/mistral/index.cjs +26 -0
  14. package/dist/cjs/llm/mistral/index.cjs.map +1 -0
  15. package/dist/cjs/llm/openai/index.cjs +82 -80
  16. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  18. package/dist/cjs/llm/providers.cjs +19 -19
  19. package/dist/cjs/llm/providers.cjs.map +1 -1
  20. package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
  21. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
  22. package/dist/cjs/llm/stream/smoother.cjs +369 -0
  23. package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
  24. package/dist/cjs/llm/vertexai/index.cjs +13 -1
  25. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  26. package/dist/cjs/main.cjs +18 -10
  27. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +34 -2
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  29. package/dist/cjs/utils/tokens.cjs +76 -29
  30. package/dist/cjs/utils/tokens.cjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +10 -0
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/instrumentation.mjs +1 -0
  34. package/dist/esm/instrumentation.mjs.map +1 -1
  35. package/dist/esm/langfuseSpanRegistry.mjs +6 -3
  36. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  37. package/dist/esm/llm/anthropic/index.mjs +34 -205
  38. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  39. package/dist/esm/llm/bedrock/index.mjs +120 -240
  40. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  41. package/dist/esm/llm/google/index.mjs +19 -12
  42. package/dist/esm/llm/google/index.mjs.map +1 -1
  43. package/dist/esm/llm/mistral/index.mjs +26 -0
  44. package/dist/esm/llm/mistral/index.mjs.map +1 -0
  45. package/dist/esm/llm/openai/index.mjs +82 -80
  46. package/dist/esm/llm/openai/index.mjs.map +1 -1
  47. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  48. package/dist/esm/llm/providers.mjs +3 -3
  49. package/dist/esm/llm/providers.mjs.map +1 -1
  50. package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
  51. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
  52. package/dist/esm/llm/stream/smoother.mjs +365 -0
  53. package/dist/esm/llm/stream/smoother.mjs.map +1 -0
  54. package/dist/esm/llm/vertexai/index.mjs +13 -1
  55. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  56. package/dist/esm/main.mjs +4 -2
  57. package/dist/esm/tools/subagent/SubagentExecutor.mjs +34 -2
  58. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  59. package/dist/esm/utils/tokens.mjs +76 -30
  60. package/dist/esm/utils/tokens.mjs.map +1 -1
  61. package/dist/types/graphs/Graph.d.ts +3 -0
  62. package/dist/types/index.d.ts +3 -0
  63. package/dist/types/llm/google/index.d.ts +2 -0
  64. package/dist/types/llm/mistral/index.d.ts +11 -0
  65. package/dist/types/llm/openai/index.d.ts +20 -4
  66. package/dist/types/llm/openrouter/index.d.ts +4 -1
  67. package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
  68. package/dist/types/llm/stream/smoother.d.ts +95 -0
  69. package/dist/types/llm/vertexai/index.d.ts +2 -0
  70. package/dist/types/tools/subagent/SubagentExecutor.d.ts +3 -0
  71. package/dist/types/types/graph.d.ts +5 -0
  72. package/dist/types/types/llm.d.ts +21 -14
  73. package/dist/types/utils/tokens.d.ts +10 -0
  74. package/package.json +2 -2
  75. package/src/graphs/Graph.ts +11 -0
  76. package/src/index.ts +8 -0
  77. package/src/instrumentation.ts +1 -0
  78. package/src/langfuseSpanRegistry.ts +9 -0
  79. package/src/llm/anthropic/index.ts +85 -354
  80. package/src/llm/bedrock/index.ts +174 -390
  81. package/src/llm/bedrock/llm.spec.ts +2 -0
  82. package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
  83. package/src/llm/custom-chat-models.smoke.test.ts +16 -1
  84. package/src/llm/google/index.ts +17 -18
  85. package/src/llm/google/streamSmoothing.test.ts +121 -0
  86. package/src/llm/mistral/index.ts +33 -0
  87. package/src/llm/mistral/streamSmoothing.test.ts +97 -0
  88. package/src/llm/openai/deepseek.test.ts +56 -0
  89. package/src/llm/openai/index.ts +119 -126
  90. package/src/llm/openrouter/index.ts +4 -1
  91. package/src/llm/providers.ts +3 -3
  92. package/src/llm/stream/chunkAdapters.test.ts +202 -0
  93. package/src/llm/stream/chunkAdapters.ts +317 -0
  94. package/src/llm/stream/reassembly.test.ts +241 -0
  95. package/src/llm/stream/smoother.bench.test.ts +155 -0
  96. package/src/llm/stream/smoother.test.ts +519 -0
  97. package/src/llm/stream/smoother.ts +574 -0
  98. package/src/llm/vertexai/index.ts +17 -1
  99. package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
  100. package/src/specs/langfuse-instrumentation.test.ts +26 -0
  101. package/src/specs/langfuse-span-registry.test.ts +17 -0
  102. package/src/specs/subagent.test.ts +44 -0
  103. package/src/specs/summarization.test.ts +5 -2
  104. package/src/specs/tokens.test.ts +159 -14
  105. package/src/tools/__tests__/SubagentExecutor.test.ts +48 -1
  106. package/src/tools/subagent/SubagentExecutor.ts +69 -5
  107. package/src/types/graph.ts +5 -0
  108. package/src/types/llm.ts +53 -36
  109. package/src/utils/tokens.ts +115 -30
@@ -27,17 +27,24 @@ import type { AggregatedHookResult, HookRegistry } from '@/hooks';
27
27
  import type { AgentContext } from '@/agents/AgentContext';
28
28
  import type { StandardGraph } from '@/graphs/Graph';
29
29
  import type { HandlerRegistry } from '@/events';
30
- import { Constants, GraphEvents, Callback, StepTypes } from '@/common';
31
30
  import {
32
31
  StreamLimitExceededError,
33
32
  RUN_BREAKER_SCOPE_CONFIG_KEY,
34
33
  } from '@/llm/streamLimits';
34
+ import {
35
+ ContentTypes,
36
+ Constants,
37
+ GraphEvents,
38
+ Callback,
39
+ StepTypes,
40
+ } from '@/common';
35
41
  import { executeHooks } from '@/hooks';
36
42
 
37
43
  const DEFAULT_MAX_TURNS = 25;
38
44
  const RECURSION_MULTIPLIER = 3;
39
45
  const ERROR_MESSAGE_MAX_CHARS = 200;
40
46
  const MAX_PENDING_SUBAGENT_UPDATES = 64;
47
+ const TEXT_DELTA_CONTENT_TYPE = `${ContentTypes.TEXT}_delta`;
41
48
 
42
49
  const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
43
50
  additionalContexts: [] as string[],
@@ -119,7 +126,10 @@ type SanitizedStepCompleted =
119
126
  };
120
127
 
121
128
  type SanitizedProcessedToolCall = Partial<
122
- Pick<ProcessedToolCall, 'args' | 'id' | 'name' | 'output' | 'progress' | 'outcome'>
129
+ Pick<
130
+ ProcessedToolCall,
131
+ 'args' | 'id' | 'name' | 'output' | 'progress' | 'outcome'
132
+ >
123
133
  >;
124
134
 
125
135
  type SanitizedRunStepCompleted = {
@@ -1401,6 +1411,9 @@ export function summarizeEvent(eventName: string, data: unknown): string {
1401
1411
  * pure tool_use (e.g. the subagent hit `maxTurns` mid-tool-call), the walk
1402
1412
  * continues to earlier AIMessages so partial progress is salvaged — this
1403
1413
  * matches Claude Code's behavior in `agentToolUtils.finalizeAgentTool`.
1414
+ * Consecutive streamed text-delta blocks with the same provider index are
1415
+ * coalesced without adding whitespace. Annotation-only text blocks are
1416
+ * ignored; complete text blocks and distinct delta indexes remain separated.
1404
1417
  * Returns "Task completed" only when no AIMessage in the history contains
1405
1418
  * any text.
1406
1419
  */
@@ -1422,13 +1435,64 @@ export function filterSubagentResult(messages: BaseMessage[]): string {
1422
1435
  }
1423
1436
 
1424
1437
  const textParts: string[] = [];
1438
+ let textDeltaParts: string[] = [];
1439
+ let textDeltaIndex: string | number | undefined;
1440
+ const flushTextDeltaParts = (): void => {
1441
+ if (textDeltaParts.length === 0) {
1442
+ return;
1443
+ }
1444
+ textParts.push(textDeltaParts.join(''));
1445
+ textDeltaParts = [];
1446
+ textDeltaIndex = undefined;
1447
+ };
1425
1448
  for (const block of content) {
1426
1449
  if (typeof block === 'string') {
1427
- textParts.push(block);
1428
- } else if ('type' in block && block.type === 'text' && 'text' in block) {
1429
- textParts.push(block.text as string);
1450
+ flushTextDeltaParts();
1451
+ if (block !== '') {
1452
+ textParts.push(block);
1453
+ }
1454
+ continue;
1455
+ }
1456
+
1457
+ const type =
1458
+ 'type' in block && typeof block.type === 'string' ? block.type : '';
1459
+ const isTextDelta = type === TEXT_DELTA_CONTENT_TYPE;
1460
+ const isText = type === ContentTypes.TEXT || isTextDelta;
1461
+ const text =
1462
+ isText && 'text' in block && typeof block.text === 'string'
1463
+ ? block.text
1464
+ : '';
1465
+ if (isTextDelta) {
1466
+ if (text === '') {
1467
+ continue;
1468
+ }
1469
+ const index =
1470
+ 'index' in block &&
1471
+ (typeof block.index === 'string' || typeof block.index === 'number')
1472
+ ? block.index
1473
+ : undefined;
1474
+ if (
1475
+ textDeltaIndex != null &&
1476
+ index != null &&
1477
+ index !== textDeltaIndex
1478
+ ) {
1479
+ flushTextDeltaParts();
1480
+ }
1481
+ textDeltaIndex ??= index;
1482
+ textDeltaParts.push(text);
1483
+ continue;
1484
+ }
1485
+
1486
+ if (type === ContentTypes.TEXT && text === '') {
1487
+ continue;
1488
+ }
1489
+
1490
+ flushTextDeltaParts();
1491
+ if (text !== '') {
1492
+ textParts.push(text);
1430
1493
  }
1431
1494
  }
1495
+ flushTextDeltaParts();
1432
1496
 
1433
1497
  if (textParts.length > 0) {
1434
1498
  return textParts.join('\n');
@@ -571,6 +571,11 @@ export interface LangfuseConfig {
571
571
  * `default` environment.
572
572
  */
573
573
  environment?: string;
574
+ /**
575
+ * Whether the Langfuse span processor should detect and upload base64 media
576
+ * payloads. Defaults to the Langfuse SDK behavior.
577
+ */
578
+ mediaUploadEnabled?: boolean;
574
579
  metadata?: Record<string, string | number | boolean | null | undefined>;
575
580
  /**
576
581
  * Internal OTLP span attributes to attach to Langfuse observations before
package/src/types/llm.ts CHANGED
@@ -45,7 +45,8 @@ export type AzureClientOptions = Partial<OpenAIChatInput> &
45
45
  deploymentName?: string;
46
46
  } & BaseChatModelParams & {
47
47
  configuration?: OAIClientOptions;
48
- } & ManagedRequestOptions;
48
+ } & ManagedRequestOptions &
49
+ StreamSmoothingOptions;
49
50
  /**
50
51
  * Controls whether Claude's reasoning content is returned in adaptive
51
52
  * thinking responses. Added for Claude Opus 4.7, which omits thinking by
@@ -79,47 +80,63 @@ export type ManagedRequestOptions = {
79
80
  promptCacheExplicit?: boolean;
80
81
  safety_identifier?: string;
81
82
  };
82
- export type OpenAIClientOptions = ChatOpenAIFields & ManagedRequestOptions;
83
- export type AnthropicClientOptions = Omit<AnthropicInput, 'thinking'> & {
84
- thinking?: ThinkingConfig;
85
- promptCache?: boolean;
86
- /**
87
- * Prompt-cache breakpoint TTL. Defaults to `'1h'` (extended cache) when
88
- * `promptCache` is enabled; set `'5m'` to opt back into the legacy
89
- * 5-minute behavior.
90
- */
91
- promptCacheTtl?: PromptCacheTtl;
92
- };
93
- export type MistralAIClientOptions = ChatMistralAIInput;
94
- export type VertexAIClientOptions = ChatVertexAIInput & {
95
- includeThoughts?: boolean;
96
- thinkingConfig?: GoogleThinkingConfig;
97
- };
98
- export type BedrockAnthropicInput = ChatBedrockConverseInput & {
99
- additionalModelRequestFields?: ChatBedrockConverseInput['additionalModelRequestFields'] &
100
- AnthropicReasoning;
101
- promptCache?: boolean;
102
- /**
103
- * Prompt-cache checkpoint TTL. Defaults to `'1h'` (extended cache) when
104
- * `promptCache` is enabled; set `'5m'` to opt into the legacy 5-minute
105
- * behavior. Bedrock models that don't support the 1-hour TTL downgrade to 5m
106
- * server-side, so the default is safe to leave on.
107
- */
108
- promptCacheTtl?: PromptCacheTtl;
83
+ /**
84
+ * Adaptive stream-smoothing configuration shared by every provider client.
85
+ */
86
+ export type StreamSmoothingOptions = {
109
87
  /**
110
88
  * Minimum delay in milliseconds between visible streamed content deltas.
89
+ * Defaults to 25; piece sizes adapt to the backlog so render lag stays
90
+ * bounded regardless of provider chunk size. Set 0 to disable smoothing.
111
91
  */
112
92
  _lc_stream_delay?: number;
113
93
  };
94
+
95
+ export type OpenAIClientOptions = ChatOpenAIFields &
96
+ ManagedRequestOptions &
97
+ StreamSmoothingOptions;
98
+ export type AnthropicClientOptions = Omit<AnthropicInput, 'thinking'> &
99
+ StreamSmoothingOptions & {
100
+ thinking?: ThinkingConfig;
101
+ promptCache?: boolean;
102
+ /**
103
+ * Prompt-cache breakpoint TTL. Defaults to `'1h'` (extended cache) when
104
+ * `promptCache` is enabled; set `'5m'` to opt back into the legacy
105
+ * 5-minute behavior.
106
+ */
107
+ promptCacheTtl?: PromptCacheTtl;
108
+ };
109
+ export type MistralAIClientOptions = ChatMistralAIInput &
110
+ StreamSmoothingOptions;
111
+ export type VertexAIClientOptions = ChatVertexAIInput &
112
+ StreamSmoothingOptions & {
113
+ includeThoughts?: boolean;
114
+ thinkingConfig?: GoogleThinkingConfig;
115
+ };
116
+ export type BedrockAnthropicInput = ChatBedrockConverseInput &
117
+ StreamSmoothingOptions & {
118
+ additionalModelRequestFields?: ChatBedrockConverseInput['additionalModelRequestFields'] &
119
+ AnthropicReasoning;
120
+ promptCache?: boolean;
121
+ /**
122
+ * Prompt-cache checkpoint TTL. Defaults to `'1h'` (extended cache) when
123
+ * `promptCache` is enabled; set `'5m'` to opt into the legacy 5-minute
124
+ * behavior. Bedrock models that don't support the 1-hour TTL downgrade to 5m
125
+ * server-side, so the default is safe to leave on.
126
+ */
127
+ promptCacheTtl?: PromptCacheTtl;
128
+ };
114
129
  export type BedrockConverseClientOptions = BedrockAnthropicInput;
115
130
  export type BedrockAnthropicClientOptions = BedrockAnthropicInput;
116
- export type GoogleClientOptions = GoogleGenerativeAIChatInput & {
117
- customHeaders?: RequestOptions['customHeaders'];
118
- thinkingConfig?: GoogleThinkingConfig;
119
- includeServerSideToolInvocations?: boolean;
120
- };
121
- export type DeepSeekClientOptions = Partial<ChatDeepSeekInput>;
122
- export type XAIClientOptions = ChatXAIInput;
131
+ export type GoogleClientOptions = GoogleGenerativeAIChatInput &
132
+ StreamSmoothingOptions & {
133
+ customHeaders?: RequestOptions['customHeaders'];
134
+ thinkingConfig?: GoogleThinkingConfig;
135
+ includeServerSideToolInvocations?: boolean;
136
+ };
137
+ export type DeepSeekClientOptions = Partial<ChatDeepSeekInput> &
138
+ StreamSmoothingOptions;
139
+ export type XAIClientOptions = ChatXAIInput & StreamSmoothingOptions;
123
140
 
124
141
  export type ClientOptions =
125
142
  | OpenAIClientOptions
@@ -159,7 +176,7 @@ export type ProviderOptionsMap = {
159
176
  [Providers.ANTHROPIC]: AnthropicClientOptions;
160
177
  [Providers.MISTRALAI]: MistralAIClientOptions;
161
178
  [Providers.MISTRAL]: MistralAIClientOptions;
162
- [Providers.OPENROUTER]: ChatOpenRouterCallOptions;
179
+ [Providers.OPENROUTER]: ChatOpenRouterCallOptions & StreamSmoothingOptions;
163
180
  [Providers.BEDROCK]: BedrockAnthropicClientOptions;
164
181
  [Providers.XAI]: XAIClientOptions;
165
182
  [Providers.MOONSHOT]: OpenAIClientOptions;
@@ -10,6 +10,52 @@ import { ContentTypes } from '@/common/enum';
10
10
 
11
11
  export type EncodingName = 'o200k_base' | 'claude';
12
12
 
13
+ export type UnsafeTokenMeasurementReason =
14
+ | 'message_proxy'
15
+ | 'content_proxy'
16
+ | 'metadata_proxy'
17
+ | 'metadata_accessor'
18
+ | 'invalid_count';
19
+
20
+ export class UnsafeTokenMeasurementError extends Error {
21
+ readonly type = 'unsafe_token_measurement';
22
+ readonly reason: UnsafeTokenMeasurementReason;
23
+ readonly path: string;
24
+
25
+ constructor({
26
+ reason,
27
+ path,
28
+ }: {
29
+ reason: UnsafeTokenMeasurementReason;
30
+ path: string;
31
+ }) {
32
+ super(
33
+ JSON.stringify({
34
+ type: 'unsafe_token_measurement',
35
+ reason,
36
+ path,
37
+ })
38
+ );
39
+ this.name = 'UnsafeTokenMeasurementError';
40
+ this.reason = reason;
41
+ this.path = path;
42
+ }
43
+ }
44
+
45
+ function ensureSafeTokenMeasurement(value: number, path: string): number {
46
+ if (
47
+ !Number.isSafeInteger(value) ||
48
+ value < 0 ||
49
+ value >= Number.MAX_SAFE_INTEGER
50
+ ) {
51
+ throw new UnsafeTokenMeasurementError({
52
+ reason: 'invalid_count',
53
+ path,
54
+ });
55
+ }
56
+ return value;
57
+ }
58
+
13
59
  /** Anthropic minimum image token cost. */
14
60
  const ANTHROPIC_IMAGE_MIN_TOKENS = 1024;
15
61
  /** Anthropic divisor: tokens = width × height / 750. */
@@ -844,7 +890,10 @@ function getBoundedTextTokenCount(
844
890
  value.length > MAX_STRUCTURED_TOKENIZATION_CHARS
845
891
  ? value.slice(0, MAX_STRUCTURED_TOKENIZATION_CHARS)
846
892
  : value;
847
- const previewTokens = getTokenCount(preview);
893
+ const previewTokens = ensureSafeTokenMeasurement(
894
+ getTokenCount(preview),
895
+ 'tokenizer'
896
+ );
848
897
  const omittedChars = value.length - preview.length;
849
898
  if (omittedChars <= 0) {
850
899
  return previewTokens;
@@ -859,19 +908,25 @@ function getBoundedStructuredTokenCount(
859
908
  value: unknown,
860
909
  getTokenCount: (text: string) => number
861
910
  ): number {
862
- if (hasUnsafeStructuredSerialization(value)) {
863
- return Number.MAX_SAFE_INTEGER;
864
- }
865
911
  const serialized = serializeStructuredValueBounded(
866
912
  value,
867
913
  MAX_STRUCTURED_TOKENIZATION_CHARS
868
914
  );
869
- const previewTokens = getTokenCount(serialized.content);
915
+ const previewTokens = ensureSafeTokenMeasurement(
916
+ getTokenCount(serialized.content),
917
+ 'tokenizer'
918
+ );
870
919
  if (!serialized.truncated) {
871
920
  return previewTokens;
872
921
  }
873
- if (!Number.isSafeInteger(serialized.originalChars)) {
874
- return Number.MAX_SAFE_INTEGER;
922
+ // The bounded serializer uses MAX_SAFE_INTEGER as its unknown-size sentinel.
923
+ // Do not extrapolate from it: use its bounded preview and let the provider
924
+ // input projection preserve the same bounded behavior before invocation.
925
+ if (
926
+ !Number.isSafeInteger(serialized.originalChars) ||
927
+ serialized.originalChars >= Number.MAX_SAFE_INTEGER
928
+ ) {
929
+ return previewTokens;
875
930
  }
876
931
 
877
932
  const omittedChars = Math.max(
@@ -893,7 +948,10 @@ export function getTokenCountForMessage(
893
948
  const countText = (text: string): number =>
894
949
  getBoundedTextTokenCount(text, getTokenCount);
895
950
  if (isProxy(message)) {
896
- return Number.MAX_SAFE_INTEGER;
951
+ throw new UnsafeTokenMeasurementError({
952
+ reason: 'message_proxy',
953
+ path: 'message',
954
+ });
897
955
  }
898
956
 
899
957
  type ContentBlock = Record<string, unknown> & {
@@ -907,19 +965,22 @@ export function getTokenCountForMessage(
907
965
  };
908
966
  const representedToolCallIds = new Set<string>();
909
967
 
910
- const processValue = (value: unknown): void => {
968
+ const processValue = (value: unknown, path: string): void => {
911
969
  if (value != null && typeof value === 'object' && isProxy(value)) {
912
- numTokens = Number.MAX_SAFE_INTEGER;
913
- return;
970
+ throw new UnsafeTokenMeasurementError({
971
+ reason: 'content_proxy',
972
+ path,
973
+ });
914
974
  }
915
975
  if (Array.isArray(value)) {
916
- for (const raw of value) {
976
+ for (let index = 0; index < value.length; index++) {
977
+ const raw = value[index];
917
978
  if (
918
979
  typeof raw === 'string' ||
919
980
  typeof raw === 'number' ||
920
981
  typeof raw === 'boolean'
921
982
  ) {
922
- processValue(raw);
983
+ processValue(raw, `${path}[${index}]`);
923
984
  continue;
924
985
  }
925
986
  const item = raw as ContentBlock | null | undefined;
@@ -927,8 +988,10 @@ export function getTokenCountForMessage(
927
988
  continue;
928
989
  }
929
990
  if (isProxy(item)) {
930
- numTokens = Number.MAX_SAFE_INTEGER;
931
- return;
991
+ throw new UnsafeTokenMeasurementError({
992
+ reason: 'content_proxy',
993
+ path: `${path}[${index}]`,
994
+ });
932
995
  }
933
996
  if (typeof item.type !== 'string') {
934
997
  numTokens += getBoundedStructuredTokenCount(item, countText);
@@ -1010,7 +1073,7 @@ export function getTokenCountForMessage(
1010
1073
  }
1011
1074
  const output = item.tool_call.output;
1012
1075
  if (output != null) {
1013
- processValue(output);
1076
+ processValue(output, `${path}[${index}].tool_call.output`);
1014
1077
  }
1015
1078
  continue;
1016
1079
  }
@@ -1021,7 +1084,7 @@ export function getTokenCountForMessage(
1021
1084
  continue;
1022
1085
  }
1023
1086
 
1024
- processValue(nestedValue);
1087
+ processValue(nestedValue, `${path}[${index}].${item.type}`);
1025
1088
  }
1026
1089
  } else if (typeof value === 'string') {
1027
1090
  numTokens += countText(value);
@@ -1040,7 +1103,10 @@ export function getTokenCountForMessage(
1040
1103
  ? rawAdditionalKwargs
1041
1104
  : undefined;
1042
1105
  if (additionalKwargs != null && isProxy(additionalKwargs)) {
1043
- return Number.MAX_SAFE_INTEGER;
1106
+ throw new UnsafeTokenMeasurementError({
1107
+ reason: 'metadata_proxy',
1108
+ path: 'additional_kwargs',
1109
+ });
1044
1110
  }
1045
1111
  let additionalType: PropertyDescriptor | undefined;
1046
1112
  try {
@@ -1049,10 +1115,16 @@ export function getTokenCountForMessage(
1049
1115
  ? Object.getOwnPropertyDescriptor(additionalKwargs, 'type')
1050
1116
  : undefined;
1051
1117
  } catch {
1052
- return Number.MAX_SAFE_INTEGER;
1118
+ throw new UnsafeTokenMeasurementError({
1119
+ reason: 'metadata_accessor',
1120
+ path: 'additional_kwargs.type',
1121
+ });
1053
1122
  }
1054
1123
  if (additionalType != null && !('value' in additionalType)) {
1055
- return Number.MAX_SAFE_INTEGER;
1124
+ throw new UnsafeTokenMeasurementError({
1125
+ reason: 'metadata_accessor',
1126
+ path: 'additional_kwargs.type',
1127
+ });
1056
1128
  }
1057
1129
 
1058
1130
  let numTokens = tokensPerMessage;
@@ -1072,20 +1144,24 @@ export function getTokenCountForMessage(
1072
1144
  ) * IMAGE_TOKEN_SAFETY_MARGIN
1073
1145
  );
1074
1146
  } else {
1075
- processValue(message.content);
1076
- }
1077
- if (numTokens >= Number.MAX_SAFE_INTEGER) {
1078
- return Number.MAX_SAFE_INTEGER;
1147
+ processValue(message.content, 'content');
1079
1148
  }
1149
+ ensureSafeTokenMeasurement(numTokens, 'content');
1080
1150
  const messageRole = (message as BaseMessage & { role?: unknown }).role;
1081
1151
  if (messageType === 'ai' || messageRole === 'assistant') {
1082
1152
  const toolCalls = (message as AIMessage).tool_calls ?? [];
1083
1153
  if (isProxy(toolCalls)) {
1084
- return Number.MAX_SAFE_INTEGER;
1154
+ throw new UnsafeTokenMeasurementError({
1155
+ reason: 'metadata_proxy',
1156
+ path: 'tool_calls',
1157
+ });
1085
1158
  }
1086
1159
  for (const toolCall of toolCalls) {
1087
1160
  if (isProxy(toolCall)) {
1088
- return Number.MAX_SAFE_INTEGER;
1161
+ throw new UnsafeTokenMeasurementError({
1162
+ reason: 'metadata_proxy',
1163
+ path: 'tool_calls',
1164
+ });
1089
1165
  }
1090
1166
  if (
1091
1167
  typeof toolCall.id === 'string' &&
@@ -1111,11 +1187,17 @@ export function getTokenCountForMessage(
1111
1187
  ? Object.getOwnPropertyDescriptor(additionalKwargs, 'function_call')
1112
1188
  : undefined;
1113
1189
  } catch {
1114
- return Number.MAX_SAFE_INTEGER;
1190
+ throw new UnsafeTokenMeasurementError({
1191
+ reason: 'metadata_accessor',
1192
+ path: 'additional_kwargs.function_call',
1193
+ });
1115
1194
  }
1116
1195
  if (legacyFunctionCall != null) {
1117
1196
  if (!('value' in legacyFunctionCall)) {
1118
- return Number.MAX_SAFE_INTEGER;
1197
+ throw new UnsafeTokenMeasurementError({
1198
+ reason: 'metadata_accessor',
1199
+ path: 'additional_kwargs.function_call',
1200
+ });
1119
1201
  }
1120
1202
  if (legacyFunctionCall.value != null) {
1121
1203
  numTokens += getBoundedStructuredTokenCount(
@@ -1125,7 +1207,7 @@ export function getTokenCountForMessage(
1125
1207
  }
1126
1208
  }
1127
1209
  }
1128
- return Math.min(Number.MAX_SAFE_INTEGER, numTokens);
1210
+ return ensureSafeTokenMeasurement(numTokens, 'message');
1129
1211
  }
1130
1212
 
1131
1213
  /**
@@ -1180,7 +1262,10 @@ export const createTokenCounter = async (
1180
1262
  const isClaude = encoding === 'claude';
1181
1263
  return (message: BaseMessage): number => {
1182
1264
  const count = getTokenCountForMessage(message, countTokens, encoding);
1183
- return isClaude ? Math.ceil(count * CLAUDE_TOKEN_CORRECTION) : count;
1265
+ const correctedCount = isClaude
1266
+ ? Math.ceil(count * CLAUDE_TOKEN_CORRECTION)
1267
+ : count;
1268
+ return ensureSafeTokenMeasurement(correctedCount, 'message');
1184
1269
  };
1185
1270
  };
1186
1271