@juspay/neurolink 9.40.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.
- package/CHANGELOG.md +12 -0
- package/README.md +7 -1
- package/dist/auth/anthropicOAuth.d.ts +18 -3
- package/dist/auth/anthropicOAuth.js +137 -4
- package/dist/auth/providers/firebase.js +5 -1
- package/dist/auth/providers/jwt.js +5 -1
- package/dist/auth/providers/workos.js +5 -1
- package/dist/auth/sessionManager.d.ts +1 -1
- package/dist/auth/sessionManager.js +58 -27
- package/dist/browser/neurolink.min.js +471 -445
- package/dist/cli/commands/mcp.js +3 -0
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +279 -16
- package/dist/cli/commands/task.d.ts +56 -0
- package/dist/cli/commands/task.js +838 -0
- package/dist/cli/factories/commandFactory.d.ts +2 -0
- package/dist/cli/factories/commandFactory.js +38 -0
- package/dist/cli/parser.js +8 -4
- package/dist/client/aiSdkAdapter.js +3 -0
- package/dist/client/streamingClient.js +30 -10
- package/dist/core/modules/GenerationHandler.js +3 -2
- package/dist/core/redisConversationMemoryManager.js +7 -3
- package/dist/evaluation/BatchEvaluator.js +4 -1
- package/dist/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/evaluation/pipeline/evaluationPipeline.js +20 -8
- package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
- package/dist/lib/auth/anthropicOAuth.js +137 -4
- package/dist/lib/auth/providers/firebase.js +5 -1
- package/dist/lib/auth/providers/jwt.js +5 -1
- package/dist/lib/auth/providers/workos.js +5 -1
- package/dist/lib/auth/sessionManager.d.ts +1 -1
- package/dist/lib/auth/sessionManager.js +58 -27
- package/dist/lib/client/aiSdkAdapter.js +3 -0
- package/dist/lib/client/streamingClient.js +30 -10
- package/dist/lib/core/modules/GenerationHandler.js +3 -2
- package/dist/lib/core/redisConversationMemoryManager.js +7 -3
- package/dist/lib/evaluation/BatchEvaluator.js +4 -1
- package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +20 -8
- package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/lib/neurolink.d.ts +18 -1
- package/dist/lib/neurolink.js +367 -484
- package/dist/lib/observability/otelBridge.d.ts +2 -2
- package/dist/lib/observability/otelBridge.js +12 -3
- package/dist/lib/providers/amazonBedrock.js +2 -4
- package/dist/lib/providers/anthropic.d.ts +9 -5
- package/dist/lib/providers/anthropic.js +19 -14
- package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/lib/providers/anthropicBaseProvider.js +5 -4
- package/dist/lib/providers/azureOpenai.d.ts +1 -1
- package/dist/lib/providers/azureOpenai.js +5 -4
- package/dist/lib/providers/googleAiStudio.js +30 -1
- package/dist/lib/providers/googleVertex.js +28 -6
- package/dist/lib/providers/huggingFace.d.ts +3 -3
- package/dist/lib/providers/huggingFace.js +6 -8
- package/dist/lib/providers/litellm.js +41 -29
- package/dist/lib/providers/mistral.js +2 -1
- package/dist/lib/providers/ollama.js +80 -23
- package/dist/lib/providers/openAI.js +3 -2
- package/dist/lib/providers/openRouter.js +2 -1
- package/dist/lib/providers/openaiCompatible.d.ts +4 -4
- package/dist/lib/providers/openaiCompatible.js +4 -4
- package/dist/lib/proxy/claudeFormat.d.ts +3 -2
- package/dist/lib/proxy/claudeFormat.js +25 -20
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/lib/proxy/modelRouter.js +3 -0
- package/dist/lib/proxy/oauthFetch.d.ts +1 -1
- package/dist/lib/proxy/oauthFetch.js +65 -72
- package/dist/lib/proxy/proxyConfig.js +44 -24
- package/dist/lib/proxy/proxyEnv.d.ts +19 -0
- package/dist/lib/proxy/proxyEnv.js +73 -0
- package/dist/lib/proxy/proxyFetch.js +50 -4
- package/dist/lib/proxy/proxyTracer.d.ts +133 -0
- package/dist/lib/proxy/proxyTracer.js +645 -0
- package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/lib/proxy/rawStreamCapture.js +83 -0
- package/dist/lib/proxy/requestLogger.d.ts +32 -5
- package/dist/lib/proxy/requestLogger.js +406 -37
- package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
- package/dist/lib/proxy/sseInterceptor.js +402 -0
- package/dist/lib/proxy/usageStats.d.ts +4 -3
- package/dist/lib/proxy/usageStats.js +25 -12
- package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/lib/rag/chunking/markdownChunker.js +15 -6
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +7 -2
- package/dist/lib/server/routes/claudeProxyRoutes.js +1737 -508
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/lib/services/server/ai/observability/instrumentation.js +240 -40
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +33 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +196 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.js +141 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.js +66 -0
- package/dist/lib/tasks/errors.d.ts +31 -0
- package/dist/lib/tasks/errors.js +18 -0
- package/dist/lib/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/lib/tasks/store/fileTaskStore.js +179 -0
- package/dist/lib/tasks/store/redisTaskStore.d.ts +43 -0
- package/dist/lib/tasks/store/redisTaskStore.js +197 -0
- package/dist/lib/tasks/taskExecutor.d.ts +21 -0
- package/dist/lib/tasks/taskExecutor.js +166 -0
- package/dist/lib/tasks/taskManager.d.ts +63 -0
- package/dist/lib/tasks/taskManager.js +426 -0
- package/dist/lib/tasks/tools/taskTools.d.ts +135 -0
- package/dist/lib/tasks/tools/taskTools.js +274 -0
- package/dist/lib/telemetry/index.d.ts +2 -1
- package/dist/lib/telemetry/index.js +2 -1
- package/dist/lib/telemetry/telemetryService.d.ts +3 -0
- package/dist/lib/telemetry/telemetryService.js +65 -5
- package/dist/lib/types/cli.d.ts +10 -0
- package/dist/lib/types/configTypes.d.ts +3 -0
- package/dist/lib/types/generateTypes.d.ts +13 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/proxyTypes.d.ts +37 -5
- package/dist/lib/types/streamTypes.d.ts +25 -3
- package/dist/lib/types/taskTypes.d.ts +275 -0
- package/dist/lib/types/taskTypes.js +37 -0
- package/dist/lib/utils/messageBuilder.js +3 -2
- package/dist/lib/utils/providerHealth.d.ts +18 -0
- package/dist/lib/utils/providerHealth.js +240 -9
- package/dist/lib/utils/providerUtils.js +14 -8
- package/dist/lib/utils/toolChoice.d.ts +4 -0
- package/dist/lib/utils/toolChoice.js +7 -0
- package/dist/neurolink.d.ts +18 -1
- package/dist/neurolink.js +367 -484
- package/dist/observability/otelBridge.d.ts +2 -2
- package/dist/observability/otelBridge.js +12 -3
- package/dist/providers/amazonBedrock.js +2 -4
- package/dist/providers/anthropic.d.ts +9 -5
- package/dist/providers/anthropic.js +19 -14
- package/dist/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/providers/anthropicBaseProvider.js +5 -4
- package/dist/providers/azureOpenai.d.ts +1 -1
- package/dist/providers/azureOpenai.js +5 -4
- package/dist/providers/googleAiStudio.js +30 -1
- package/dist/providers/googleVertex.js +28 -6
- package/dist/providers/huggingFace.d.ts +3 -3
- package/dist/providers/huggingFace.js +6 -7
- package/dist/providers/litellm.js +41 -29
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/ollama.js +80 -23
- package/dist/providers/openAI.js +3 -2
- package/dist/providers/openRouter.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +4 -4
- package/dist/providers/openaiCompatible.js +4 -3
- package/dist/proxy/claudeFormat.d.ts +3 -2
- package/dist/proxy/claudeFormat.js +25 -20
- package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/proxy/modelRouter.js +3 -0
- package/dist/proxy/oauthFetch.d.ts +1 -1
- package/dist/proxy/oauthFetch.js +65 -72
- package/dist/proxy/proxyConfig.js +44 -24
- package/dist/proxy/proxyEnv.d.ts +19 -0
- package/dist/proxy/proxyEnv.js +72 -0
- package/dist/proxy/proxyFetch.js +50 -4
- package/dist/proxy/proxyTracer.d.ts +133 -0
- package/dist/proxy/proxyTracer.js +644 -0
- package/dist/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/proxy/rawStreamCapture.js +82 -0
- package/dist/proxy/requestLogger.d.ts +32 -5
- package/dist/proxy/requestLogger.js +406 -37
- package/dist/proxy/sseInterceptor.d.ts +97 -0
- package/dist/proxy/sseInterceptor.js +401 -0
- package/dist/proxy/usageStats.d.ts +4 -3
- package/dist/proxy/usageStats.js +25 -12
- package/dist/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/rag/chunking/markdownChunker.js +15 -6
- package/dist/server/routes/claudeProxyRoutes.d.ts +7 -2
- package/dist/server/routes/claudeProxyRoutes.js +1737 -508
- package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/services/server/ai/observability/instrumentation.js +240 -40
- package/dist/tasks/backends/bullmqBackend.d.ts +33 -0
- package/dist/tasks/backends/bullmqBackend.js +195 -0
- package/dist/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/tasks/backends/nodeTimeoutBackend.js +140 -0
- package/dist/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/tasks/backends/taskBackendRegistry.js +65 -0
- package/dist/tasks/errors.d.ts +31 -0
- package/dist/tasks/errors.js +17 -0
- package/dist/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/tasks/store/fileTaskStore.js +178 -0
- package/dist/tasks/store/redisTaskStore.d.ts +43 -0
- package/dist/tasks/store/redisTaskStore.js +196 -0
- package/dist/tasks/taskExecutor.d.ts +21 -0
- package/dist/tasks/taskExecutor.js +165 -0
- package/dist/tasks/taskManager.d.ts +63 -0
- package/dist/tasks/taskManager.js +425 -0
- package/dist/tasks/tools/taskTools.d.ts +135 -0
- package/dist/tasks/tools/taskTools.js +273 -0
- package/dist/telemetry/index.d.ts +2 -1
- package/dist/telemetry/index.js +2 -1
- package/dist/telemetry/telemetryService.d.ts +3 -0
- package/dist/telemetry/telemetryService.js +65 -5
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/generateTypes.d.ts +13 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/proxyTypes.d.ts +37 -5
- package/dist/types/streamTypes.d.ts +25 -3
- package/dist/types/taskTypes.d.ts +275 -0
- package/dist/types/taskTypes.js +36 -0
- package/dist/utils/messageBuilder.js +3 -2
- package/dist/utils/providerHealth.d.ts +18 -0
- package/dist/utils/providerHealth.js +240 -9
- package/dist/utils/providerUtils.js +14 -8
- package/dist/utils/toolChoice.d.ts +4 -0
- package/dist/utils/toolChoice.js +6 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
- package/docs/changelog.md +252 -0
- package/package.json +19 -1
- package/scripts/observability/check-proxy-telemetry.mjs +235 -0
- package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
- package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
- package/scripts/observability/manage-local-openobserve.sh +184 -0
- package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
- 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
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
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
|
|
15
|
-
|
|
16
|
-
ensureAccount(accountLabel, accountType)
|
|
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
|
|
21
|
+
export function recordFinalSuccess(accountLabel, accountType) {
|
|
19
22
|
stats.totalRequests++;
|
|
20
23
|
stats.totalSuccess++;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
if (accountLabel && accountType) {
|
|
25
|
+
const acct = ensureAccount(accountLabel, accountType);
|
|
26
|
+
acct.successCount++;
|
|
27
|
+
acct.currentBackoffLevel = 0;
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
|
-
export function
|
|
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
|
-
|
|
78
|
+
attemptCount: 0,
|
|
66
79
|
successCount: 0,
|
|
67
80
|
errorCount: 0,
|
|
68
81
|
rateLimitCount: 0,
|
|
69
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|