@juspay/neurolink 9.41.0 → 9.42.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 (189) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +137 -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 +337 -318
  11. package/dist/cli/commands/mcp.js +3 -0
  12. package/dist/cli/commands/proxy.d.ts +2 -1
  13. package/dist/cli/commands/proxy.js +279 -16
  14. package/dist/cli/commands/task.js +3 -0
  15. package/dist/cli/factories/commandFactory.d.ts +2 -0
  16. package/dist/cli/factories/commandFactory.js +38 -0
  17. package/dist/cli/parser.js +4 -3
  18. package/dist/client/aiSdkAdapter.js +3 -0
  19. package/dist/client/streamingClient.js +30 -10
  20. package/dist/core/modules/GenerationHandler.js +3 -2
  21. package/dist/core/redisConversationMemoryManager.js +7 -3
  22. package/dist/evaluation/BatchEvaluator.js +4 -1
  23. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  24. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  25. package/dist/evaluation/pipeline/evaluationPipeline.js +20 -8
  26. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  27. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  28. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  29. package/dist/lib/auth/anthropicOAuth.js +137 -4
  30. package/dist/lib/auth/providers/firebase.js +5 -1
  31. package/dist/lib/auth/providers/jwt.js +5 -1
  32. package/dist/lib/auth/providers/workos.js +5 -1
  33. package/dist/lib/auth/sessionManager.d.ts +1 -1
  34. package/dist/lib/auth/sessionManager.js +58 -27
  35. package/dist/lib/client/aiSdkAdapter.js +3 -0
  36. package/dist/lib/client/streamingClient.js +30 -10
  37. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  38. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  39. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  40. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  41. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  42. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +20 -8
  43. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  44. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  45. package/dist/lib/neurolink.d.ts +3 -2
  46. package/dist/lib/neurolink.js +260 -494
  47. package/dist/lib/observability/otelBridge.d.ts +2 -2
  48. package/dist/lib/observability/otelBridge.js +12 -3
  49. package/dist/lib/providers/amazonBedrock.js +2 -4
  50. package/dist/lib/providers/anthropic.d.ts +9 -5
  51. package/dist/lib/providers/anthropic.js +19 -14
  52. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  53. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  54. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  55. package/dist/lib/providers/azureOpenai.js +5 -4
  56. package/dist/lib/providers/googleAiStudio.js +30 -1
  57. package/dist/lib/providers/googleVertex.js +28 -6
  58. package/dist/lib/providers/huggingFace.d.ts +3 -3
  59. package/dist/lib/providers/huggingFace.js +6 -8
  60. package/dist/lib/providers/litellm.js +41 -29
  61. package/dist/lib/providers/mistral.js +2 -1
  62. package/dist/lib/providers/ollama.js +80 -23
  63. package/dist/lib/providers/openAI.js +3 -2
  64. package/dist/lib/providers/openRouter.js +2 -1
  65. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  66. package/dist/lib/providers/openaiCompatible.js +4 -4
  67. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  68. package/dist/lib/proxy/claudeFormat.js +25 -20
  69. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  70. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  71. package/dist/lib/proxy/modelRouter.js +3 -0
  72. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  73. package/dist/lib/proxy/oauthFetch.js +65 -72
  74. package/dist/lib/proxy/proxyConfig.js +44 -24
  75. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  76. package/dist/lib/proxy/proxyEnv.js +73 -0
  77. package/dist/lib/proxy/proxyFetch.js +50 -4
  78. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  79. package/dist/lib/proxy/proxyTracer.js +645 -0
  80. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  81. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  82. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  83. package/dist/lib/proxy/requestLogger.js +406 -37
  84. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  85. package/dist/lib/proxy/sseInterceptor.js +402 -0
  86. package/dist/lib/proxy/usageStats.d.ts +4 -3
  87. package/dist/lib/proxy/usageStats.js +25 -12
  88. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  89. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  90. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +7 -2
  91. package/dist/lib/server/routes/claudeProxyRoutes.js +1737 -508
  92. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  93. package/dist/lib/services/server/ai/observability/instrumentation.js +240 -40
  94. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  95. package/dist/lib/tasks/backends/bullmqBackend.js +14 -7
  96. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  97. package/dist/lib/tasks/store/redisTaskStore.js +34 -26
  98. package/dist/lib/tasks/taskManager.d.ts +3 -0
  99. package/dist/lib/tasks/taskManager.js +63 -30
  100. package/dist/lib/telemetry/index.d.ts +2 -1
  101. package/dist/lib/telemetry/index.js +2 -1
  102. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  103. package/dist/lib/telemetry/telemetryService.js +65 -5
  104. package/dist/lib/types/cli.d.ts +10 -0
  105. package/dist/lib/types/proxyTypes.d.ts +37 -5
  106. package/dist/lib/types/streamTypes.d.ts +25 -3
  107. package/dist/lib/utils/messageBuilder.js +3 -2
  108. package/dist/lib/utils/providerHealth.d.ts +18 -0
  109. package/dist/lib/utils/providerHealth.js +240 -9
  110. package/dist/lib/utils/providerUtils.js +14 -8
  111. package/dist/lib/utils/toolChoice.d.ts +4 -0
  112. package/dist/lib/utils/toolChoice.js +7 -0
  113. package/dist/neurolink.d.ts +3 -2
  114. package/dist/neurolink.js +260 -494
  115. package/dist/observability/otelBridge.d.ts +2 -2
  116. package/dist/observability/otelBridge.js +12 -3
  117. package/dist/providers/amazonBedrock.js +2 -4
  118. package/dist/providers/anthropic.d.ts +9 -5
  119. package/dist/providers/anthropic.js +19 -14
  120. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  121. package/dist/providers/anthropicBaseProvider.js +5 -4
  122. package/dist/providers/azureOpenai.d.ts +1 -1
  123. package/dist/providers/azureOpenai.js +5 -4
  124. package/dist/providers/googleAiStudio.js +30 -1
  125. package/dist/providers/googleVertex.js +28 -6
  126. package/dist/providers/huggingFace.d.ts +3 -3
  127. package/dist/providers/huggingFace.js +6 -7
  128. package/dist/providers/litellm.js +41 -29
  129. package/dist/providers/mistral.js +2 -1
  130. package/dist/providers/ollama.js +80 -23
  131. package/dist/providers/openAI.js +3 -2
  132. package/dist/providers/openRouter.js +2 -1
  133. package/dist/providers/openaiCompatible.d.ts +4 -4
  134. package/dist/providers/openaiCompatible.js +4 -3
  135. package/dist/proxy/claudeFormat.d.ts +3 -2
  136. package/dist/proxy/claudeFormat.js +25 -20
  137. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  138. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  139. package/dist/proxy/modelRouter.js +3 -0
  140. package/dist/proxy/oauthFetch.d.ts +1 -1
  141. package/dist/proxy/oauthFetch.js +65 -72
  142. package/dist/proxy/proxyConfig.js +44 -24
  143. package/dist/proxy/proxyEnv.d.ts +19 -0
  144. package/dist/proxy/proxyEnv.js +72 -0
  145. package/dist/proxy/proxyFetch.js +50 -4
  146. package/dist/proxy/proxyTracer.d.ts +133 -0
  147. package/dist/proxy/proxyTracer.js +644 -0
  148. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  149. package/dist/proxy/rawStreamCapture.js +82 -0
  150. package/dist/proxy/requestLogger.d.ts +32 -5
  151. package/dist/proxy/requestLogger.js +406 -37
  152. package/dist/proxy/sseInterceptor.d.ts +97 -0
  153. package/dist/proxy/sseInterceptor.js +401 -0
  154. package/dist/proxy/usageStats.d.ts +4 -3
  155. package/dist/proxy/usageStats.js +25 -12
  156. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  157. package/dist/rag/chunking/markdownChunker.js +15 -6
  158. package/dist/server/routes/claudeProxyRoutes.d.ts +7 -2
  159. package/dist/server/routes/claudeProxyRoutes.js +1737 -508
  160. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  161. package/dist/services/server/ai/observability/instrumentation.js +240 -40
  162. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  163. package/dist/tasks/backends/bullmqBackend.js +14 -7
  164. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  165. package/dist/tasks/store/redisTaskStore.js +34 -26
  166. package/dist/tasks/taskManager.d.ts +3 -0
  167. package/dist/tasks/taskManager.js +63 -30
  168. package/dist/telemetry/index.d.ts +2 -1
  169. package/dist/telemetry/index.js +2 -1
  170. package/dist/telemetry/telemetryService.d.ts +3 -0
  171. package/dist/telemetry/telemetryService.js +65 -5
  172. package/dist/types/cli.d.ts +10 -0
  173. package/dist/types/proxyTypes.d.ts +37 -5
  174. package/dist/types/streamTypes.d.ts +25 -3
  175. package/dist/utils/messageBuilder.js +3 -2
  176. package/dist/utils/providerHealth.d.ts +18 -0
  177. package/dist/utils/providerHealth.js +240 -9
  178. package/dist/utils/providerUtils.js +14 -8
  179. package/dist/utils/toolChoice.d.ts +4 -0
  180. package/dist/utils/toolChoice.js +6 -0
  181. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  182. package/docs/changelog.md +252 -0
  183. package/package.json +17 -1
  184. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  185. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  186. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  187. package/scripts/observability/manage-local-openobserve.sh +184 -0
  188. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  189. 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,402 @@
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 truncateString(input, maxBytes) {
95
+ if (input.length <= maxBytes) {
96
+ return input;
97
+ }
98
+ return `${input.slice(0, maxBytes)}${TRUNCATION_MARKER}`;
99
+ }
100
+ function appendCappedFragment(current, fragment, currentBytes, maxBytes) {
101
+ if (currentBytes >= maxBytes) {
102
+ return {
103
+ value: current && current.endsWith(TRUNCATION_MARKER)
104
+ ? current
105
+ : `${current ?? ""}${TRUNCATION_MARKER}`,
106
+ nextBytes: currentBytes + fragment.length,
107
+ };
108
+ }
109
+ const remainingBytes = maxBytes - currentBytes;
110
+ const nextBytes = currentBytes + fragment.length;
111
+ if (fragment.length <= remainingBytes) {
112
+ return {
113
+ value: `${current ?? ""}${fragment}`,
114
+ nextBytes,
115
+ };
116
+ }
117
+ return {
118
+ value: `${current ?? ""}${fragment.slice(0, remainingBytes)}${TRUNCATION_MARKER}`,
119
+ nextBytes,
120
+ };
121
+ }
122
+ function appendRawTextChunk(acc, chunk) {
123
+ if (!acc.rawTextChunks || acc.rawTextTruncated) {
124
+ return;
125
+ }
126
+ const remainingBytes = MAX_RAW_TEXT_BYTES - acc.rawTextBytes;
127
+ if (remainingBytes <= 0) {
128
+ acc.rawTextChunks.push(TRUNCATION_MARKER);
129
+ acc.rawTextTruncated = true;
130
+ return;
131
+ }
132
+ if (chunk.length <= remainingBytes) {
133
+ acc.rawTextChunks.push(chunk);
134
+ acc.rawTextBytes += chunk.length;
135
+ return;
136
+ }
137
+ acc.rawTextChunks.push(chunk.slice(0, remainingBytes), TRUNCATION_MARKER);
138
+ acc.rawTextBytes = MAX_RAW_TEXT_BYTES;
139
+ acc.rawTextTruncated = true;
140
+ }
141
+ function finalize(acc) {
142
+ const totalTokens = acc.inputTokens + acc.outputTokens;
143
+ return {
144
+ messageId: acc.messageId,
145
+ model: acc.model,
146
+ usage: {
147
+ inputTokens: acc.inputTokens,
148
+ outputTokens: acc.outputTokens,
149
+ cacheCreationInputTokens: acc.cacheCreationInputTokens,
150
+ cacheReadInputTokens: acc.cacheReadInputTokens,
151
+ totalTokens,
152
+ },
153
+ contentBlocks: acc.contentBlocks,
154
+ stopReason: acc.stopReason,
155
+ stopSequence: acc.stopSequence,
156
+ eventCount: acc.eventCount,
157
+ streamDurationMs: Date.now() - acc.startTime,
158
+ totalBytesReceived: acc.totalBytesReceived,
159
+ events: acc.events,
160
+ ...(acc.rawTextChunks ? { rawText: acc.rawTextChunks.join("") } : {}),
161
+ };
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Event processors
165
+ // ---------------------------------------------------------------------------
166
+ /* eslint-disable @typescript-eslint/no-explicit-any */
167
+ function processMessageStart(acc, parsed) {
168
+ const msg = parsed.message;
169
+ if (!msg) {
170
+ return;
171
+ }
172
+ acc.messageId = msg.id ?? "";
173
+ acc.model = msg.model ?? "";
174
+ const usage = msg.usage;
175
+ if (usage) {
176
+ acc.inputTokens += usage.input_tokens ?? 0;
177
+ acc.outputTokens += usage.output_tokens ?? 0;
178
+ acc.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0;
179
+ acc.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0;
180
+ }
181
+ }
182
+ function processContentBlockStart(acc, parsed) {
183
+ const index = parsed.index ?? 0;
184
+ const block = parsed.content_block;
185
+ if (!block) {
186
+ return;
187
+ }
188
+ const blockType = block.type;
189
+ const entry = { index, type: blockType };
190
+ if (blockType === "text") {
191
+ entry.text = block.text ?? "";
192
+ }
193
+ else if (blockType === "thinking") {
194
+ entry.thinking = block.thinking ?? "";
195
+ }
196
+ else if (blockType === "tool_use") {
197
+ entry.toolName = block.name ?? "";
198
+ entry.toolId = block.id ?? "";
199
+ entry.toolInput = "";
200
+ }
201
+ acc.contentBlocks.push(entry);
202
+ acc.blockByteCounts.set(index, 0);
203
+ }
204
+ function processContentBlockDelta(acc, parsed) {
205
+ const index = parsed.index ?? 0;
206
+ const delta = parsed.delta;
207
+ if (!delta) {
208
+ return;
209
+ }
210
+ // Find the matching block
211
+ const block = acc.contentBlocks.find((b) => b.index === index);
212
+ if (!block) {
213
+ return;
214
+ }
215
+ const currentBytes = acc.blockByteCounts.get(index) ?? 0;
216
+ const capped = currentBytes >= MAX_BLOCK_CONTENT_BYTES;
217
+ if (delta.type === "text_delta" && delta.text !== null) {
218
+ const fragment = delta.text;
219
+ const updated = appendCappedFragment(block.text, fragment, currentBytes, MAX_BLOCK_CONTENT_BYTES);
220
+ acc.blockByteCounts.set(index, updated.nextBytes);
221
+ if (!capped || !block.text?.endsWith(TRUNCATION_MARKER)) {
222
+ block.text = updated.value;
223
+ }
224
+ }
225
+ else if (delta.type === "thinking_delta" && delta.thinking !== null) {
226
+ const fragment = delta.thinking;
227
+ const updated = appendCappedFragment(block.thinking, fragment, currentBytes, MAX_BLOCK_CONTENT_BYTES);
228
+ acc.blockByteCounts.set(index, updated.nextBytes);
229
+ if (!capped || !block.thinking?.endsWith(TRUNCATION_MARKER)) {
230
+ block.thinking = updated.value;
231
+ }
232
+ }
233
+ else if (delta.type === "input_json_delta" && delta.partial_json !== null) {
234
+ const fragment = delta.partial_json;
235
+ const updated = appendCappedFragment(block.toolInput, fragment, currentBytes, MAX_BLOCK_CONTENT_BYTES);
236
+ acc.blockByteCounts.set(index, updated.nextBytes);
237
+ if (!capped || !block.toolInput?.endsWith(TRUNCATION_MARKER)) {
238
+ block.toolInput = updated.value;
239
+ }
240
+ }
241
+ }
242
+ function processMessageDelta(acc, parsed) {
243
+ const delta = parsed.delta;
244
+ if (delta) {
245
+ acc.stopReason = delta.stop_reason ?? acc.stopReason;
246
+ acc.stopSequence = delta.stop_sequence ?? acc.stopSequence;
247
+ }
248
+ const usage = parsed.usage;
249
+ if (usage) {
250
+ // message_delta provides the final output_tokens count; treat it as
251
+ // additive because message_start reports output_tokens: 0 for the
252
+ // initial placeholder.
253
+ acc.outputTokens += usage.output_tokens ?? 0;
254
+ }
255
+ }
256
+ /* eslint-enable @typescript-eslint/no-explicit-any */
257
+ // ---------------------------------------------------------------------------
258
+ // Dispatch a parsed SSE event to the appropriate handler
259
+ // ---------------------------------------------------------------------------
260
+ function processEvent(acc, event) {
261
+ acc.eventCount++;
262
+ const now = Date.now();
263
+ // For content_block_delta events, store only the event type to save memory.
264
+ // For all other events, store the full data string.
265
+ // Cap event log to prevent unbounded growth.
266
+ if (acc.events.length < MAX_EVENT_LOG_ENTRIES - 1) {
267
+ if (event.event === "content_block_delta") {
268
+ acc.events.push({ type: event.event, timestamp: now, data: "" });
269
+ }
270
+ else {
271
+ acc.events.push({
272
+ type: event.event,
273
+ timestamp: now,
274
+ data: truncateString(event.data, MAX_EVENT_DATA_BYTES),
275
+ });
276
+ }
277
+ }
278
+ else if (!acc.eventLogTruncated) {
279
+ acc.events.push({
280
+ type: "truncated",
281
+ timestamp: now,
282
+ data: TRUNCATION_MARKER,
283
+ });
284
+ acc.eventLogTruncated = true;
285
+ }
286
+ // Skip JSON parsing for events with no data payload
287
+ if (!event.data) {
288
+ return;
289
+ }
290
+ let parsed;
291
+ try {
292
+ parsed = JSON.parse(event.data);
293
+ }
294
+ catch {
295
+ // Malformed JSON — skip silently, bytes already forwarded to client
296
+ return;
297
+ }
298
+ switch (event.event) {
299
+ case "message_start":
300
+ processMessageStart(acc, parsed);
301
+ break;
302
+ case "content_block_start":
303
+ processContentBlockStart(acc, parsed);
304
+ break;
305
+ case "content_block_delta":
306
+ processContentBlockDelta(acc, parsed);
307
+ break;
308
+ case "message_delta":
309
+ processMessageDelta(acc, parsed);
310
+ break;
311
+ // content_block_stop, message_stop, ping — no telemetry to extract
312
+ default:
313
+ break;
314
+ }
315
+ }
316
+ /**
317
+ * Create an SSE interceptor that extracts telemetry from an Anthropic
318
+ * streaming response while passing all bytes through unmodified.
319
+ *
320
+ * ```ts
321
+ * const { stream, telemetry } = createSSEInterceptor();
322
+ * upstreamResponse.body
323
+ * .pipeThrough(stream)
324
+ * .pipeTo(clientWritable);
325
+ *
326
+ * const data = await telemetry;
327
+ * console.log(data.usage.totalTokens);
328
+ * ```
329
+ */
330
+ export function createSSEInterceptor(options = {}) {
331
+ const captureRawText = options.captureRawText ?? false;
332
+ const acc = createAccumulator(captureRawText);
333
+ let sseBuffer = "";
334
+ let resolved = false;
335
+ const decoder = new TextDecoder();
336
+ let resolveTelemetry;
337
+ const telemetryPromise = new Promise((resolve) => {
338
+ resolveTelemetry = resolve;
339
+ });
340
+ /** Resolve the telemetry promise exactly once. */
341
+ function settle() {
342
+ if (resolved) {
343
+ return;
344
+ }
345
+ resolved = true;
346
+ resolveTelemetry(finalize(acc));
347
+ }
348
+ const transform = new TransformStream({
349
+ transform(chunk, controller) {
350
+ // Forward the raw bytes immediately — zero delay to client.
351
+ controller.enqueue(chunk);
352
+ // Track total bytes received for bandwidth metrics.
353
+ acc.totalBytesReceived += chunk.byteLength;
354
+ // Decode and buffer for SSE parsing.
355
+ const decodedChunk = decoder.decode(chunk, { stream: true });
356
+ appendRawTextChunk(acc, decodedChunk);
357
+ sseBuffer += decodedChunk;
358
+ const { events, remainder } = extractSSEEvents(sseBuffer);
359
+ sseBuffer = remainder;
360
+ for (const event of events) {
361
+ processEvent(acc, event);
362
+ }
363
+ },
364
+ flush() {
365
+ const finalChunk = decoder.decode();
366
+ if (finalChunk) {
367
+ appendRawTextChunk(acc, finalChunk);
368
+ sseBuffer += finalChunk;
369
+ }
370
+ // Process any trailing data left in the buffer (e.g. a final event
371
+ // not followed by a double-newline).
372
+ if (sseBuffer.trim()) {
373
+ const { events } = extractSSEEvents(sseBuffer + "\n\n");
374
+ for (const event of events) {
375
+ processEvent(acc, event);
376
+ }
377
+ }
378
+ settle();
379
+ },
380
+ });
381
+ // Wrap the writable side so we can intercept abort() — which does NOT
382
+ // trigger the TransformStream's flush() or cancel() callbacks.
383
+ const innerWriter = transform.writable.getWriter();
384
+ const writable = new WritableStream({
385
+ write(chunk) {
386
+ return innerWriter.write(chunk);
387
+ },
388
+ close() {
389
+ return innerWriter.close();
390
+ },
391
+ abort(reason) {
392
+ settle();
393
+ return innerWriter.abort(reason);
394
+ },
395
+ });
396
+ const stream = {
397
+ readable: transform.readable,
398
+ writable,
399
+ };
400
+ return { stream, telemetry: telemetryPromise };
401
+ }
402
+ //# sourceMappingURL=sseInterceptor.js.map
@@ -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
@@ -150,12 +150,20 @@ export class MarkdownChunker {
150
150
  const ranges = [];
151
151
  let i = 0;
152
152
  while (i < lines.length) {
153
+ const currentLine = lines[i];
154
+ const separatorLine = lines[i + 1];
153
155
  if (i + 1 < lines.length &&
154
- TABLE_ROW_RE.test(lines[i]) &&
155
- this.isTableSeparator(lines[i + 1], SEPARATOR_CELL_RE)) {
156
+ currentLine !== undefined &&
157
+ separatorLine !== undefined &&
158
+ TABLE_ROW_RE.test(currentLine) &&
159
+ this.isTableSeparator(separatorLine, SEPARATOR_CELL_RE)) {
156
160
  const start = i;
157
161
  i += 2;
158
- while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
162
+ while (i < lines.length) {
163
+ const row = lines[i];
164
+ if (row === undefined || !TABLE_ROW_RE.test(row)) {
165
+ break;
166
+ }
159
167
  i++;
160
168
  }
161
169
  ranges.push({ start, end: i - 1 });
@@ -175,7 +183,8 @@ export class MarkdownChunker {
175
183
  // Split by "|" → ["", "---", "---", ""] for "|---|---|"
176
184
  const cells = trimmed.split("|");
177
185
  cells.shift(); // remove leading empty element
178
- if (cells.length > 0 && cells[cells.length - 1].trim() === "") {
186
+ const lastCell = cells.at(-1);
187
+ if (cells.length > 0 && lastCell?.trim() === "") {
179
188
  cells.pop(); // remove trailing empty element
180
189
  }
181
190
  if (cells.length === 0) {
@@ -261,8 +270,8 @@ export class MarkdownChunker {
261
270
  if (rows.length < 3) {
262
271
  return [tableText];
263
272
  }
264
- const headerRow = rows[0];
265
- const separatorRow = rows[1];
273
+ const headerRow = rows[0] ?? "";
274
+ const separatorRow = rows[1] ?? "";
266
275
  const headerBlock = headerRow + "\n" + separatorRow;
267
276
  const dataRows = rows.slice(2);
268
277
  if (headerBlock.length > maxSize) {