@juspay/neurolink 9.41.0 → 9.42.1

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 (212) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +149 -4
  5. package/dist/auth/providers/firebase.js +5 -1
  6. package/dist/auth/providers/jwt.js +5 -1
  7. package/dist/auth/providers/workos.js +5 -1
  8. package/dist/auth/sessionManager.d.ts +1 -1
  9. package/dist/auth/sessionManager.js +58 -27
  10. package/dist/browser/neurolink.min.js +354 -334
  11. package/dist/cli/commands/mcp.d.ts +6 -0
  12. package/dist/cli/commands/mcp.js +188 -181
  13. package/dist/cli/commands/proxy.d.ts +2 -1
  14. package/dist/cli/commands/proxy.js +713 -431
  15. package/dist/cli/commands/task.js +3 -0
  16. package/dist/cli/factories/commandFactory.d.ts +2 -0
  17. package/dist/cli/factories/commandFactory.js +38 -0
  18. package/dist/cli/parser.js +4 -3
  19. package/dist/client/aiSdkAdapter.js +3 -0
  20. package/dist/client/streamingClient.js +30 -10
  21. package/dist/core/baseProvider.d.ts +6 -1
  22. package/dist/core/baseProvider.js +208 -230
  23. package/dist/core/factory.d.ts +3 -0
  24. package/dist/core/factory.js +138 -188
  25. package/dist/core/modules/GenerationHandler.js +3 -2
  26. package/dist/core/redisConversationMemoryManager.js +7 -3
  27. package/dist/evaluation/BatchEvaluator.js +4 -1
  28. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  29. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  30. package/dist/evaluation/pipeline/evaluationPipeline.js +24 -9
  31. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  32. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  33. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  34. package/dist/evaluation/scorers/scorerRegistry.js +353 -282
  35. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  36. package/dist/lib/auth/anthropicOAuth.js +149 -4
  37. package/dist/lib/auth/providers/firebase.js +5 -1
  38. package/dist/lib/auth/providers/jwt.js +5 -1
  39. package/dist/lib/auth/providers/workos.js +5 -1
  40. package/dist/lib/auth/sessionManager.d.ts +1 -1
  41. package/dist/lib/auth/sessionManager.js +58 -27
  42. package/dist/lib/client/aiSdkAdapter.js +3 -0
  43. package/dist/lib/client/streamingClient.js +30 -10
  44. package/dist/lib/core/baseProvider.d.ts +6 -1
  45. package/dist/lib/core/baseProvider.js +208 -230
  46. package/dist/lib/core/factory.d.ts +3 -0
  47. package/dist/lib/core/factory.js +138 -188
  48. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  49. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  50. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  51. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  52. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  53. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +24 -9
  54. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  55. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  56. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  57. package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
  58. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  59. package/dist/lib/mcp/toolRegistry.js +32 -31
  60. package/dist/lib/neurolink.d.ts +41 -2
  61. package/dist/lib/neurolink.js +1616 -1681
  62. package/dist/lib/observability/otelBridge.d.ts +2 -2
  63. package/dist/lib/observability/otelBridge.js +12 -3
  64. package/dist/lib/providers/amazonBedrock.js +2 -4
  65. package/dist/lib/providers/anthropic.d.ts +9 -5
  66. package/dist/lib/providers/anthropic.js +19 -14
  67. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  68. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  69. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  70. package/dist/lib/providers/azureOpenai.js +5 -4
  71. package/dist/lib/providers/googleAiStudio.js +30 -6
  72. package/dist/lib/providers/googleVertex.d.ts +10 -0
  73. package/dist/lib/providers/googleVertex.js +437 -423
  74. package/dist/lib/providers/huggingFace.d.ts +3 -3
  75. package/dist/lib/providers/huggingFace.js +6 -8
  76. package/dist/lib/providers/litellm.d.ts +1 -0
  77. package/dist/lib/providers/litellm.js +76 -55
  78. package/dist/lib/providers/mistral.js +2 -1
  79. package/dist/lib/providers/ollama.js +93 -23
  80. package/dist/lib/providers/openAI.d.ts +2 -0
  81. package/dist/lib/providers/openAI.js +141 -141
  82. package/dist/lib/providers/openRouter.js +2 -1
  83. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  84. package/dist/lib/providers/openaiCompatible.js +4 -4
  85. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  86. package/dist/lib/proxy/claudeFormat.js +27 -14
  87. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  88. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  89. package/dist/lib/proxy/modelRouter.js +3 -0
  90. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  91. package/dist/lib/proxy/oauthFetch.js +289 -316
  92. package/dist/lib/proxy/proxyConfig.js +46 -24
  93. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  94. package/dist/lib/proxy/proxyEnv.js +73 -0
  95. package/dist/lib/proxy/proxyFetch.js +291 -217
  96. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  97. package/dist/lib/proxy/proxyTracer.js +645 -0
  98. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  99. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  100. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  101. package/dist/lib/proxy/requestLogger.js +503 -47
  102. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  103. package/dist/lib/proxy/sseInterceptor.js +427 -0
  104. package/dist/lib/proxy/usageStats.d.ts +4 -3
  105. package/dist/lib/proxy/usageStats.js +25 -12
  106. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  107. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  108. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +17 -3
  109. package/dist/lib/server/routes/claudeProxyRoutes.js +3032 -1349
  110. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  111. package/dist/lib/services/server/ai/observability/instrumentation.js +337 -161
  112. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  113. package/dist/lib/tasks/backends/bullmqBackend.js +35 -22
  114. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  115. package/dist/lib/tasks/store/redisTaskStore.js +54 -39
  116. package/dist/lib/tasks/taskManager.d.ts +5 -0
  117. package/dist/lib/tasks/taskManager.js +158 -30
  118. package/dist/lib/telemetry/index.d.ts +2 -1
  119. package/dist/lib/telemetry/index.js +2 -1
  120. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  121. package/dist/lib/telemetry/telemetryService.js +69 -5
  122. package/dist/lib/types/cli.d.ts +10 -0
  123. package/dist/lib/types/proxyTypes.d.ts +160 -5
  124. package/dist/lib/types/streamTypes.d.ts +25 -3
  125. package/dist/lib/utils/messageBuilder.js +3 -2
  126. package/dist/lib/utils/providerHealth.d.ts +19 -0
  127. package/dist/lib/utils/providerHealth.js +279 -33
  128. package/dist/lib/utils/providerUtils.js +17 -22
  129. package/dist/lib/utils/toolChoice.d.ts +4 -0
  130. package/dist/lib/utils/toolChoice.js +7 -0
  131. package/dist/mcp/toolRegistry.d.ts +2 -0
  132. package/dist/mcp/toolRegistry.js +32 -31
  133. package/dist/neurolink.d.ts +41 -2
  134. package/dist/neurolink.js +1616 -1681
  135. package/dist/observability/otelBridge.d.ts +2 -2
  136. package/dist/observability/otelBridge.js +12 -3
  137. package/dist/providers/amazonBedrock.js +2 -4
  138. package/dist/providers/anthropic.d.ts +9 -5
  139. package/dist/providers/anthropic.js +19 -14
  140. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  141. package/dist/providers/anthropicBaseProvider.js +5 -4
  142. package/dist/providers/azureOpenai.d.ts +1 -1
  143. package/dist/providers/azureOpenai.js +5 -4
  144. package/dist/providers/googleAiStudio.js +30 -6
  145. package/dist/providers/googleVertex.d.ts +10 -0
  146. package/dist/providers/googleVertex.js +437 -423
  147. package/dist/providers/huggingFace.d.ts +3 -3
  148. package/dist/providers/huggingFace.js +6 -7
  149. package/dist/providers/litellm.d.ts +1 -0
  150. package/dist/providers/litellm.js +76 -55
  151. package/dist/providers/mistral.js +2 -1
  152. package/dist/providers/ollama.js +93 -23
  153. package/dist/providers/openAI.d.ts +2 -0
  154. package/dist/providers/openAI.js +141 -141
  155. package/dist/providers/openRouter.js +2 -1
  156. package/dist/providers/openaiCompatible.d.ts +4 -4
  157. package/dist/providers/openaiCompatible.js +4 -3
  158. package/dist/proxy/claudeFormat.d.ts +3 -2
  159. package/dist/proxy/claudeFormat.js +27 -14
  160. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  161. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  162. package/dist/proxy/modelRouter.js +3 -0
  163. package/dist/proxy/oauthFetch.d.ts +1 -1
  164. package/dist/proxy/oauthFetch.js +289 -316
  165. package/dist/proxy/proxyConfig.js +46 -24
  166. package/dist/proxy/proxyEnv.d.ts +19 -0
  167. package/dist/proxy/proxyEnv.js +72 -0
  168. package/dist/proxy/proxyFetch.js +291 -217
  169. package/dist/proxy/proxyTracer.d.ts +133 -0
  170. package/dist/proxy/proxyTracer.js +644 -0
  171. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  172. package/dist/proxy/rawStreamCapture.js +82 -0
  173. package/dist/proxy/requestLogger.d.ts +32 -5
  174. package/dist/proxy/requestLogger.js +503 -47
  175. package/dist/proxy/sseInterceptor.d.ts +97 -0
  176. package/dist/proxy/sseInterceptor.js +426 -0
  177. package/dist/proxy/usageStats.d.ts +4 -3
  178. package/dist/proxy/usageStats.js +25 -12
  179. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  180. package/dist/rag/chunking/markdownChunker.js +15 -6
  181. package/dist/server/routes/claudeProxyRoutes.d.ts +17 -3
  182. package/dist/server/routes/claudeProxyRoutes.js +3032 -1349
  183. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  184. package/dist/services/server/ai/observability/instrumentation.js +337 -161
  185. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  186. package/dist/tasks/backends/bullmqBackend.js +35 -22
  187. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  188. package/dist/tasks/store/redisTaskStore.js +54 -39
  189. package/dist/tasks/taskManager.d.ts +5 -0
  190. package/dist/tasks/taskManager.js +158 -30
  191. package/dist/telemetry/index.d.ts +2 -1
  192. package/dist/telemetry/index.js +2 -1
  193. package/dist/telemetry/telemetryService.d.ts +3 -0
  194. package/dist/telemetry/telemetryService.js +69 -5
  195. package/dist/types/cli.d.ts +10 -0
  196. package/dist/types/proxyTypes.d.ts +160 -5
  197. package/dist/types/streamTypes.d.ts +25 -3
  198. package/dist/utils/messageBuilder.js +3 -2
  199. package/dist/utils/providerHealth.d.ts +19 -0
  200. package/dist/utils/providerHealth.js +279 -33
  201. package/dist/utils/providerUtils.js +18 -22
  202. package/dist/utils/toolChoice.d.ts +4 -0
  203. package/dist/utils/toolChoice.js +6 -0
  204. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  205. package/docs/changelog.md +252 -0
  206. package/package.json +19 -2
  207. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  208. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  209. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  210. package/scripts/observability/manage-local-openobserve.sh +215 -0
  211. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  212. package/scripts/observability/proxy-observability.env.example +23 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * SSE Stream Interceptor
3
+ *
4
+ * A zero-overhead TransformStream that taps Anthropic SSE streaming responses
5
+ * to extract telemetry data (token usage, model info, content blocks, thinking
6
+ * blocks, tool use) while passing every byte through to the client unmodified
7
+ * and without delay.
8
+ *
9
+ * The interceptor buffers partial SSE events internally (chunks may split
10
+ * across event boundaries) but never holds back any bytes from the readable
11
+ * side of the stream.
12
+ *
13
+ * Usage:
14
+ * const { stream, telemetry } = createSSEInterceptor();
15
+ * upstreamResponse.body.pipeThrough(stream).pipeTo(clientWritable);
16
+ * const data = await telemetry; // resolves on stream end
17
+ */
18
+ export type SSEContentBlock = {
19
+ index: number;
20
+ type: "text" | "thinking" | "tool_use" | "tool_result";
21
+ /** Accumulated text for text blocks. Capped at MAX_BLOCK_CONTENT_BYTES. */
22
+ text?: string;
23
+ /** Accumulated thinking content. Capped at MAX_BLOCK_CONTENT_BYTES. */
24
+ thinking?: string;
25
+ /** Tool name for tool_use blocks. */
26
+ toolName?: string;
27
+ /** Tool call id for tool_use blocks. */
28
+ toolId?: string;
29
+ /** Accumulated partial JSON input for tool_use blocks. Capped at MAX_BLOCK_CONTENT_BYTES. */
30
+ toolInput?: string;
31
+ };
32
+ export type SSETelemetry = {
33
+ /** Message id from message_start. */
34
+ messageId: string;
35
+ /** Model string from message_start. */
36
+ model: string;
37
+ /** Token usage aggregated from message_start + message_delta. */
38
+ usage: {
39
+ inputTokens: number;
40
+ outputTokens: number;
41
+ cacheCreationInputTokens: number;
42
+ cacheReadInputTokens: number;
43
+ totalTokens: number;
44
+ };
45
+ /** All content blocks accumulated during the stream. */
46
+ contentBlocks: SSEContentBlock[];
47
+ /** Stop reason from message_delta, e.g. "end_turn". */
48
+ stopReason: string | null;
49
+ /** Stop sequence from message_delta, if any. */
50
+ stopSequence: string | null;
51
+ /** Total number of SSE events observed. */
52
+ eventCount: number;
53
+ /** Wall-clock duration from first byte to stream end (ms). */
54
+ streamDurationMs: number;
55
+ /** Total bytes received from upstream (raw SSE stream size). */
56
+ totalBytesReceived: number;
57
+ /**
58
+ * Raw SSE event log. For content_block_delta events only the type is
59
+ * stored (not the full data payload) to avoid excessive memory use.
60
+ * All other events store the full data string.
61
+ */
62
+ events: Array<{
63
+ type: string;
64
+ timestamp: number;
65
+ data: string;
66
+ }>;
67
+ /** Full raw SSE transcript, when captureRawText is enabled. */
68
+ rawText?: string;
69
+ };
70
+ export type SSEInterceptorResult = {
71
+ /** Pipe the upstream response through this stream. */
72
+ stream: TransformStream<Uint8Array, Uint8Array>;
73
+ /**
74
+ * Resolves with the accumulated telemetry when the stream finishes.
75
+ * If the stream errors, the promise resolves with whatever telemetry
76
+ * was gathered up to that point (never rejects).
77
+ */
78
+ telemetry: Promise<SSETelemetry>;
79
+ };
80
+ export type SSEInterceptorOptions = {
81
+ captureRawText?: boolean;
82
+ };
83
+ /**
84
+ * Create an SSE interceptor that extracts telemetry from an Anthropic
85
+ * streaming response while passing all bytes through unmodified.
86
+ *
87
+ * ```ts
88
+ * const { stream, telemetry } = createSSEInterceptor();
89
+ * upstreamResponse.body
90
+ * .pipeThrough(stream)
91
+ * .pipeTo(clientWritable);
92
+ *
93
+ * const data = await telemetry;
94
+ * console.log(data.usage.totalTokens);
95
+ * ```
96
+ */
97
+ export declare function createSSEInterceptor(options?: SSEInterceptorOptions): SSEInterceptorResult;
@@ -0,0 +1,426 @@
1
+ /**
2
+ * SSE Stream Interceptor
3
+ *
4
+ * A zero-overhead TransformStream that taps Anthropic SSE streaming responses
5
+ * to extract telemetry data (token usage, model info, content blocks, thinking
6
+ * blocks, tool use) while passing every byte through to the client unmodified
7
+ * and without delay.
8
+ *
9
+ * The interceptor buffers partial SSE events internally (chunks may split
10
+ * across event boundaries) but never holds back any bytes from the readable
11
+ * side of the stream.
12
+ *
13
+ * Usage:
14
+ * const { stream, telemetry } = createSSEInterceptor();
15
+ * upstreamResponse.body.pipeThrough(stream).pipeTo(clientWritable);
16
+ * const data = await telemetry; // resolves on stream end
17
+ */
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ /** Maximum accumulated content per block before we stop appending (100 KB). */
22
+ const MAX_BLOCK_CONTENT_BYTES = 100 * 1024;
23
+ /** Maximum number of events to record in the event log to cap memory usage. */
24
+ const MAX_EVENT_LOG_ENTRIES = 5000;
25
+ const MAX_EVENT_DATA_BYTES = 2048;
26
+ const MAX_RAW_TEXT_BYTES = 1024 * 1024;
27
+ const TRUNCATION_MARKER = "...[TRUNCATED]";
28
+ // ---------------------------------------------------------------------------
29
+ // Internal SSE line parser
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Incrementally parse SSE events from a growing buffer of text.
33
+ *
34
+ * SSE events are separated by a blank line (`\n\n`). Each event consists of
35
+ * field lines (`event: ...`, `data: ...`). We consume complete events and
36
+ * return them, leaving any trailing partial event in the buffer.
37
+ */
38
+ function extractSSEEvents(buffer) {
39
+ const events = [];
40
+ // Split on double-newline boundaries. The last segment may be an
41
+ // incomplete event if the chunk was split mid-event.
42
+ let cursor = 0;
43
+ while (cursor < buffer.length) {
44
+ const boundary = buffer.indexOf("\n\n", cursor);
45
+ if (boundary === -1) {
46
+ // No more complete events — everything from cursor onward is partial.
47
+ break;
48
+ }
49
+ const rawBlock = buffer.slice(cursor, boundary);
50
+ cursor = boundary + 2; // skip past the \n\n
51
+ let eventType = "";
52
+ let dataValue = "";
53
+ const lines = rawBlock.split("\n");
54
+ for (const line of lines) {
55
+ if (line.startsWith("event: ")) {
56
+ eventType = line.slice(7).trim();
57
+ }
58
+ else if (line.startsWith("data: ")) {
59
+ dataValue = line.slice(6);
60
+ }
61
+ else if (line.startsWith("data:")) {
62
+ // handle `data:` with no space (edge case)
63
+ dataValue = line.slice(5);
64
+ }
65
+ }
66
+ if (eventType || dataValue) {
67
+ events.push({ event: eventType, data: dataValue });
68
+ }
69
+ }
70
+ return { events, remainder: buffer.slice(cursor) };
71
+ }
72
+ function createAccumulator(captureRawText) {
73
+ return {
74
+ messageId: "",
75
+ model: "",
76
+ inputTokens: 0,
77
+ outputTokens: 0,
78
+ cacheCreationInputTokens: 0,
79
+ cacheReadInputTokens: 0,
80
+ contentBlocks: [],
81
+ blockByteCounts: new Map(),
82
+ stopReason: null,
83
+ stopSequence: null,
84
+ eventCount: 0,
85
+ startTime: Date.now(),
86
+ totalBytesReceived: 0,
87
+ events: [],
88
+ rawTextChunks: captureRawText ? [] : undefined,
89
+ rawTextBytes: 0,
90
+ rawTextTruncated: false,
91
+ eventLogTruncated: false,
92
+ };
93
+ }
94
+ function utf8ByteLength(input) {
95
+ return Buffer.byteLength(input, "utf8");
96
+ }
97
+ function truncateUtf8String(input, maxBytes) {
98
+ if (utf8ByteLength(input) <= maxBytes) {
99
+ return input;
100
+ }
101
+ const markerBytes = utf8ByteLength(TRUNCATION_MARKER);
102
+ if (maxBytes <= 0 || maxBytes < markerBytes) {
103
+ return "";
104
+ }
105
+ let output = "";
106
+ let usedBytes = 0;
107
+ for (const char of input) {
108
+ const charBytes = utf8ByteLength(char);
109
+ if (usedBytes + charBytes + markerBytes > maxBytes) {
110
+ break;
111
+ }
112
+ output += char;
113
+ usedBytes += charBytes;
114
+ }
115
+ return `${output}${TRUNCATION_MARKER}`;
116
+ }
117
+ function truncateString(input, maxBytes) {
118
+ return truncateUtf8String(input, maxBytes);
119
+ }
120
+ function appendCappedFragment(current, fragment, currentBytes, maxBytes) {
121
+ const fragmentBytes = utf8ByteLength(fragment);
122
+ if (currentBytes >= maxBytes) {
123
+ return {
124
+ value: current && current.endsWith(TRUNCATION_MARKER)
125
+ ? current
126
+ : `${current ?? ""}${TRUNCATION_MARKER}`,
127
+ nextBytes: currentBytes + fragmentBytes,
128
+ };
129
+ }
130
+ const remainingBytes = maxBytes - currentBytes;
131
+ const nextBytes = currentBytes + fragmentBytes;
132
+ if (fragmentBytes <= remainingBytes) {
133
+ return {
134
+ value: `${current ?? ""}${fragment}`,
135
+ nextBytes,
136
+ };
137
+ }
138
+ return {
139
+ value: `${current ?? ""}${truncateUtf8String(fragment, remainingBytes)}`,
140
+ nextBytes,
141
+ };
142
+ }
143
+ function appendRawTextChunk(acc, chunk) {
144
+ if (!acc.rawTextChunks || acc.rawTextTruncated) {
145
+ return;
146
+ }
147
+ const remainingBytes = MAX_RAW_TEXT_BYTES - acc.rawTextBytes;
148
+ if (remainingBytes <= 0) {
149
+ acc.rawTextChunks.push(TRUNCATION_MARKER);
150
+ acc.rawTextTruncated = true;
151
+ return;
152
+ }
153
+ const chunkBytes = utf8ByteLength(chunk);
154
+ if (chunkBytes <= remainingBytes) {
155
+ acc.rawTextChunks.push(chunk);
156
+ acc.rawTextBytes += chunkBytes;
157
+ return;
158
+ }
159
+ acc.rawTextChunks.push(truncateUtf8String(chunk, remainingBytes));
160
+ acc.rawTextBytes = MAX_RAW_TEXT_BYTES;
161
+ acc.rawTextTruncated = true;
162
+ }
163
+ function getBlockContentBytes(block) {
164
+ return utf8ByteLength(block.text ?? block.thinking ?? block.toolInput ?? "");
165
+ }
166
+ function finalize(acc) {
167
+ const totalTokens = acc.inputTokens + acc.outputTokens;
168
+ return {
169
+ messageId: acc.messageId,
170
+ model: acc.model,
171
+ usage: {
172
+ inputTokens: acc.inputTokens,
173
+ outputTokens: acc.outputTokens,
174
+ cacheCreationInputTokens: acc.cacheCreationInputTokens,
175
+ cacheReadInputTokens: acc.cacheReadInputTokens,
176
+ totalTokens,
177
+ },
178
+ contentBlocks: acc.contentBlocks,
179
+ stopReason: acc.stopReason,
180
+ stopSequence: acc.stopSequence,
181
+ eventCount: acc.eventCount,
182
+ streamDurationMs: Date.now() - acc.startTime,
183
+ totalBytesReceived: acc.totalBytesReceived,
184
+ events: acc.events,
185
+ ...(acc.rawTextChunks ? { rawText: acc.rawTextChunks.join("") } : {}),
186
+ };
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // Event processors
190
+ // ---------------------------------------------------------------------------
191
+ /* eslint-disable @typescript-eslint/no-explicit-any */
192
+ function processMessageStart(acc, parsed) {
193
+ const msg = parsed.message;
194
+ if (!msg) {
195
+ return;
196
+ }
197
+ acc.messageId = msg.id ?? "";
198
+ acc.model = msg.model ?? "";
199
+ const usage = msg.usage;
200
+ if (usage) {
201
+ acc.inputTokens += usage.input_tokens ?? 0;
202
+ acc.outputTokens += usage.output_tokens ?? 0;
203
+ acc.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0;
204
+ acc.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0;
205
+ }
206
+ }
207
+ function processContentBlockStart(acc, parsed) {
208
+ const index = parsed.index ?? 0;
209
+ const block = parsed.content_block;
210
+ if (!block) {
211
+ return;
212
+ }
213
+ const blockType = block.type;
214
+ const entry = { index, type: blockType };
215
+ if (blockType === "text") {
216
+ entry.text = block.text ?? "";
217
+ }
218
+ else if (blockType === "thinking") {
219
+ entry.thinking = block.thinking ?? "";
220
+ }
221
+ else if (blockType === "tool_use") {
222
+ entry.toolName = block.name ?? "";
223
+ entry.toolId = block.id ?? "";
224
+ entry.toolInput = "";
225
+ }
226
+ acc.contentBlocks.push(entry);
227
+ acc.blockByteCounts.set(index, getBlockContentBytes(entry));
228
+ }
229
+ function processContentBlockDelta(acc, parsed) {
230
+ const index = parsed.index ?? 0;
231
+ const delta = parsed.delta;
232
+ if (!delta) {
233
+ return;
234
+ }
235
+ // Find the matching block
236
+ const block = acc.contentBlocks.find((b) => b.index === index);
237
+ if (!block) {
238
+ return;
239
+ }
240
+ const currentBytes = acc.blockByteCounts.get(index) ?? 0;
241
+ const capped = currentBytes >= MAX_BLOCK_CONTENT_BYTES;
242
+ if (delta.type === "text_delta" && delta.text !== null) {
243
+ const fragment = delta.text;
244
+ const updated = appendCappedFragment(block.text, fragment, currentBytes, MAX_BLOCK_CONTENT_BYTES);
245
+ acc.blockByteCounts.set(index, updated.nextBytes);
246
+ if (!capped || !block.text?.endsWith(TRUNCATION_MARKER)) {
247
+ block.text = updated.value;
248
+ }
249
+ }
250
+ else if (delta.type === "thinking_delta" && delta.thinking !== null) {
251
+ const fragment = delta.thinking;
252
+ const updated = appendCappedFragment(block.thinking, fragment, currentBytes, MAX_BLOCK_CONTENT_BYTES);
253
+ acc.blockByteCounts.set(index, updated.nextBytes);
254
+ if (!capped || !block.thinking?.endsWith(TRUNCATION_MARKER)) {
255
+ block.thinking = updated.value;
256
+ }
257
+ }
258
+ else if (delta.type === "input_json_delta" && delta.partial_json !== null) {
259
+ const fragment = delta.partial_json;
260
+ const updated = appendCappedFragment(block.toolInput, fragment, currentBytes, MAX_BLOCK_CONTENT_BYTES);
261
+ acc.blockByteCounts.set(index, updated.nextBytes);
262
+ if (!capped || !block.toolInput?.endsWith(TRUNCATION_MARKER)) {
263
+ block.toolInput = updated.value;
264
+ }
265
+ }
266
+ }
267
+ function processMessageDelta(acc, parsed) {
268
+ const delta = parsed.delta;
269
+ if (delta) {
270
+ acc.stopReason = delta.stop_reason ?? acc.stopReason;
271
+ acc.stopSequence = delta.stop_sequence ?? acc.stopSequence;
272
+ }
273
+ const usage = parsed.usage;
274
+ if (usage) {
275
+ // message_delta provides the final output_tokens count; treat it as
276
+ // additive because message_start reports output_tokens: 0 for the
277
+ // initial placeholder.
278
+ acc.outputTokens += usage.output_tokens ?? 0;
279
+ }
280
+ }
281
+ /* eslint-enable @typescript-eslint/no-explicit-any */
282
+ // ---------------------------------------------------------------------------
283
+ // Dispatch a parsed SSE event to the appropriate handler
284
+ // ---------------------------------------------------------------------------
285
+ function processEvent(acc, event) {
286
+ acc.eventCount++;
287
+ const now = Date.now();
288
+ // For content_block_delta events, store only the event type to save memory.
289
+ // For all other events, store the full data string.
290
+ // Cap event log to prevent unbounded growth.
291
+ if (acc.events.length < MAX_EVENT_LOG_ENTRIES - 1) {
292
+ if (event.event === "content_block_delta") {
293
+ acc.events.push({ type: event.event, timestamp: now, data: "" });
294
+ }
295
+ else {
296
+ acc.events.push({
297
+ type: event.event,
298
+ timestamp: now,
299
+ data: truncateString(event.data, MAX_EVENT_DATA_BYTES),
300
+ });
301
+ }
302
+ }
303
+ else if (!acc.eventLogTruncated) {
304
+ acc.events.push({
305
+ type: "truncated",
306
+ timestamp: now,
307
+ data: TRUNCATION_MARKER,
308
+ });
309
+ acc.eventLogTruncated = true;
310
+ }
311
+ // Skip JSON parsing for events with no data payload
312
+ if (!event.data) {
313
+ return;
314
+ }
315
+ let parsed;
316
+ try {
317
+ parsed = JSON.parse(event.data);
318
+ }
319
+ catch {
320
+ // Malformed JSON — skip silently, bytes already forwarded to client
321
+ return;
322
+ }
323
+ switch (event.event) {
324
+ case "message_start":
325
+ processMessageStart(acc, parsed);
326
+ break;
327
+ case "content_block_start":
328
+ processContentBlockStart(acc, parsed);
329
+ break;
330
+ case "content_block_delta":
331
+ processContentBlockDelta(acc, parsed);
332
+ break;
333
+ case "message_delta":
334
+ processMessageDelta(acc, parsed);
335
+ break;
336
+ // content_block_stop, message_stop, ping — no telemetry to extract
337
+ default:
338
+ break;
339
+ }
340
+ }
341
+ /**
342
+ * Create an SSE interceptor that extracts telemetry from an Anthropic
343
+ * streaming response while passing all bytes through unmodified.
344
+ *
345
+ * ```ts
346
+ * const { stream, telemetry } = createSSEInterceptor();
347
+ * upstreamResponse.body
348
+ * .pipeThrough(stream)
349
+ * .pipeTo(clientWritable);
350
+ *
351
+ * const data = await telemetry;
352
+ * console.log(data.usage.totalTokens);
353
+ * ```
354
+ */
355
+ export function createSSEInterceptor(options = {}) {
356
+ const captureRawText = options.captureRawText ?? false;
357
+ const acc = createAccumulator(captureRawText);
358
+ let sseBuffer = "";
359
+ let resolved = false;
360
+ const decoder = new TextDecoder();
361
+ let resolveTelemetry;
362
+ const telemetryPromise = new Promise((resolve) => {
363
+ resolveTelemetry = resolve;
364
+ });
365
+ /** Resolve the telemetry promise exactly once. */
366
+ function settle() {
367
+ if (resolved) {
368
+ return;
369
+ }
370
+ resolved = true;
371
+ resolveTelemetry(finalize(acc));
372
+ }
373
+ const transform = new TransformStream({
374
+ transform(chunk, controller) {
375
+ // Forward the raw bytes immediately — zero delay to client.
376
+ controller.enqueue(chunk);
377
+ // Track total bytes received for bandwidth metrics.
378
+ acc.totalBytesReceived += chunk.byteLength;
379
+ // Decode and buffer for SSE parsing.
380
+ const decodedChunk = decoder.decode(chunk, { stream: true });
381
+ appendRawTextChunk(acc, decodedChunk);
382
+ sseBuffer += decodedChunk;
383
+ const { events, remainder } = extractSSEEvents(sseBuffer);
384
+ sseBuffer = remainder;
385
+ for (const event of events) {
386
+ processEvent(acc, event);
387
+ }
388
+ },
389
+ flush() {
390
+ const finalChunk = decoder.decode();
391
+ if (finalChunk) {
392
+ appendRawTextChunk(acc, finalChunk);
393
+ sseBuffer += finalChunk;
394
+ }
395
+ // Process any trailing data left in the buffer (e.g. a final event
396
+ // not followed by a double-newline).
397
+ if (sseBuffer.trim()) {
398
+ const { events } = extractSSEEvents(sseBuffer + "\n\n");
399
+ for (const event of events) {
400
+ processEvent(acc, event);
401
+ }
402
+ }
403
+ settle();
404
+ },
405
+ });
406
+ // Wrap the writable side so we can intercept abort() — which does NOT
407
+ // trigger the TransformStream's flush() or cancel() callbacks.
408
+ const innerWriter = transform.writable.getWriter();
409
+ const writable = new WritableStream({
410
+ write(chunk) {
411
+ return innerWriter.write(chunk);
412
+ },
413
+ close() {
414
+ return innerWriter.close();
415
+ },
416
+ abort(reason) {
417
+ settle();
418
+ return innerWriter.abort(reason);
419
+ },
420
+ });
421
+ const stream = {
422
+ readable: transform.readable,
423
+ writable,
424
+ };
425
+ return { stream, telemetry: telemetryPromise };
426
+ }
@@ -4,9 +4,10 @@
4
4
  * In-memory only — resets on proxy restart.
5
5
  */
6
6
  import type { AccountStats, ProxyStats } from "../types/index.js";
7
- export declare function recordRequest(accountLabel: string, accountType: string): void;
8
- export declare function recordSuccess(accountLabel: string, accountType: string): void;
9
- export declare function recordError(accountLabel: string, accountType: string, status: number): void;
7
+ export declare function recordAttempt(accountLabel: string, accountType: string): void;
8
+ export declare function recordFinalSuccess(accountLabel?: string, accountType?: string): void;
9
+ export declare function recordAttemptError(accountLabel: string, accountType: string, status: number): void;
10
+ export declare function recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
10
11
  export declare function recordCooldown(accountLabel: string, accountType: string, cooldownUntil: number, backoffLevel: number): void;
11
12
  export declare function getStats(): ProxyStats;
12
13
  export declare function getAccountStats(label: string): AccountStats | undefined;
@@ -5,26 +5,29 @@
5
5
  */
6
6
  const stats = {
7
7
  startedAt: Date.now(),
8
+ totalAttempts: 0,
8
9
  totalRequests: 0,
9
10
  totalSuccess: 0,
10
11
  totalErrors: 0,
11
12
  totalRateLimits: 0,
12
13
  accounts: {},
13
14
  };
14
- export function recordRequest(accountLabel, accountType) {
15
- ensureAccount(accountLabel, accountType).requestCount++;
16
- ensureAccount(accountLabel, accountType).lastRequestAt = Date.now();
15
+ export function recordAttempt(accountLabel, accountType) {
16
+ stats.totalAttempts++;
17
+ const acct = ensureAccount(accountLabel, accountType);
18
+ acct.attemptCount++;
19
+ acct.lastAttemptAt = Date.now();
17
20
  }
18
- export function recordSuccess(accountLabel, accountType) {
21
+ export function recordFinalSuccess(accountLabel, accountType) {
19
22
  stats.totalRequests++;
20
23
  stats.totalSuccess++;
21
- const acct = ensureAccount(accountLabel, accountType);
22
- acct.successCount++;
23
- acct.currentBackoffLevel = 0;
24
+ if (accountLabel && accountType) {
25
+ const acct = ensureAccount(accountLabel, accountType);
26
+ acct.successCount++;
27
+ acct.currentBackoffLevel = 0;
28
+ }
24
29
  }
25
- export function recordError(accountLabel, accountType, status) {
26
- stats.totalRequests++;
27
- stats.totalErrors++;
30
+ export function recordAttemptError(accountLabel, accountType, status) {
28
31
  const acct = ensureAccount(accountLabel, accountType);
29
32
  acct.errorCount++;
30
33
  acct.lastErrorAt = Date.now();
@@ -33,6 +36,15 @@ export function recordError(accountLabel, accountType, status) {
33
36
  acct.rateLimitCount++;
34
37
  }
35
38
  }
39
+ export function recordFinalError(_status, accountLabel, accountType) {
40
+ stats.totalRequests++;
41
+ stats.totalErrors++;
42
+ if (accountLabel && accountType) {
43
+ const acct = ensureAccount(accountLabel, accountType);
44
+ acct.errorCount++;
45
+ acct.lastErrorAt = Date.now();
46
+ }
47
+ }
36
48
  export function recordCooldown(accountLabel, accountType, cooldownUntil, backoffLevel) {
37
49
  const acct = ensureAccount(accountLabel, accountType);
38
50
  acct.coolingUntil = cooldownUntil;
@@ -51,6 +63,7 @@ export function getAccountStats(label) {
51
63
  }
52
64
  export function resetStats() {
53
65
  stats.startedAt = Date.now();
66
+ stats.totalAttempts = 0;
54
67
  stats.totalRequests = 0;
55
68
  stats.totalSuccess = 0;
56
69
  stats.totalErrors = 0;
@@ -62,11 +75,11 @@ function ensureAccount(label, type) {
62
75
  stats.accounts[label] = {
63
76
  label,
64
77
  type,
65
- requestCount: 0,
78
+ attemptCount: 0,
66
79
  successCount: 0,
67
80
  errorCount: 0,
68
81
  rateLimitCount: 0,
69
- lastRequestAt: 0,
82
+ lastAttemptAt: 0,
70
83
  currentBackoffLevel: 0,
71
84
  };
72
85
  }
@@ -19,15 +19,23 @@ function detectTableRanges(lines) {
19
19
  const ranges = [];
20
20
  let i = 0;
21
21
  while (i < lines.length) {
22
+ const currentLine = lines[i];
23
+ const separatorLine = lines[i + 1];
22
24
  // A table needs at least a header row + separator
23
25
  if (i + 1 < lines.length &&
24
- TABLE_ROW_RE.test(lines[i]) &&
25
- TABLE_SEPARATOR_RE.test(lines[i + 1])) {
26
+ currentLine !== undefined &&
27
+ separatorLine !== undefined &&
28
+ TABLE_ROW_RE.test(currentLine) &&
29
+ TABLE_SEPARATOR_RE.test(separatorLine)) {
26
30
  const start = i;
27
31
  // Advance past header + separator
28
32
  i += 2;
29
33
  // Consume remaining data rows
30
- while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
34
+ while (i < lines.length) {
35
+ const row = lines[i];
36
+ if (row === undefined || !TABLE_ROW_RE.test(row)) {
37
+ break;
38
+ }
31
39
  i++;
32
40
  }
33
41
  ranges.push({ start, end: i - 1 });
@@ -231,8 +239,8 @@ export class MarkdownChunker extends BaseChunker {
231
239
  // Not a proper table (need header + separator + at least 1 data row)
232
240
  return [tableText];
233
241
  }
234
- const headerRow = rows[0];
235
- const separatorRow = rows[1];
242
+ const headerRow = rows[0] ?? "";
243
+ const separatorRow = rows[1] ?? "";
236
244
  const headerBlock = headerRow + "\n" + separatorRow;
237
245
  const dataRows = rows.slice(2);
238
246
  // If even the header doesn't fit, fall back to size-based split