@juspay/neurolink 9.14.0 → 9.16.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 +15 -15
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/adapters/video/videoAnalyzer.js +10 -8
- package/dist/auth/anthropicOAuth.d.ts +377 -0
- package/dist/auth/anthropicOAuth.js +914 -0
- package/dist/auth/index.d.ts +20 -0
- package/dist/auth/index.js +29 -0
- package/dist/auth/tokenStore.d.ts +225 -0
- package/dist/auth/tokenStore.js +521 -0
- package/dist/cli/commands/auth.d.ts +50 -0
- package/dist/cli/commands/auth.js +1115 -0
- package/dist/cli/commands/setup-anthropic.js +1 -14
- package/dist/cli/commands/setup-azure.js +1 -12
- package/dist/cli/commands/setup-bedrock.js +1 -9
- package/dist/cli/commands/setup-google-ai.js +1 -12
- package/dist/cli/commands/setup-openai.js +1 -14
- package/dist/cli/commands/workflow.d.ts +27 -0
- package/dist/cli/commands/workflow.js +216 -0
- package/dist/cli/factories/authCommandFactory.d.ts +52 -0
- package/dist/cli/factories/authCommandFactory.js +146 -0
- package/dist/cli/factories/commandFactory.d.ts +6 -0
- package/dist/cli/factories/commandFactory.js +171 -22
- package/dist/cli/index.js +0 -1
- package/dist/cli/parser.js +14 -2
- package/dist/cli/utils/maskCredential.d.ts +11 -0
- package/dist/cli/utils/maskCredential.js +23 -0
- package/dist/constants/contextWindows.js +107 -16
- package/dist/constants/enums.d.ts +119 -15
- package/dist/constants/enums.js +182 -22
- package/dist/constants/index.d.ts +3 -1
- package/dist/constants/index.js +11 -1
- package/dist/context/budgetChecker.js +1 -1
- package/dist/context/contextCompactor.js +31 -4
- package/dist/context/emergencyTruncation.d.ts +21 -0
- package/dist/context/emergencyTruncation.js +88 -0
- package/dist/context/errorDetection.d.ts +16 -0
- package/dist/context/errorDetection.js +48 -1
- package/dist/context/errors.d.ts +19 -0
- package/dist/context/errors.js +21 -0
- package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/core/baseProvider.js +306 -200
- package/dist/core/conversationMemoryManager.js +104 -61
- package/dist/core/evaluationProviders.js +16 -33
- package/dist/core/factory.js +237 -164
- package/dist/core/modules/GenerationHandler.js +175 -116
- package/dist/core/modules/MessageBuilder.js +222 -170
- package/dist/core/modules/StreamHandler.d.ts +1 -0
- package/dist/core/modules/StreamHandler.js +95 -27
- package/dist/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/core/modules/TelemetryHandler.js +25 -7
- package/dist/core/modules/ToolsManager.js +115 -191
- package/dist/core/redisConversationMemoryManager.js +418 -282
- package/dist/factories/providerRegistry.d.ts +5 -0
- package/dist/factories/providerRegistry.js +20 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -2
- package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
- package/dist/lib/auth/anthropicOAuth.d.ts +377 -0
- package/dist/lib/auth/anthropicOAuth.js +915 -0
- package/dist/lib/auth/index.d.ts +20 -0
- package/dist/lib/auth/index.js +30 -0
- package/dist/lib/auth/tokenStore.d.ts +225 -0
- package/dist/lib/auth/tokenStore.js +522 -0
- package/dist/lib/constants/contextWindows.js +107 -16
- package/dist/lib/constants/enums.d.ts +119 -15
- package/dist/lib/constants/enums.js +182 -22
- package/dist/lib/constants/index.d.ts +3 -1
- package/dist/lib/constants/index.js +11 -1
- package/dist/lib/context/budgetChecker.js +1 -1
- package/dist/lib/context/contextCompactor.js +31 -4
- package/dist/lib/context/emergencyTruncation.d.ts +21 -0
- package/dist/lib/context/emergencyTruncation.js +89 -0
- package/dist/lib/context/errorDetection.d.ts +16 -0
- package/dist/lib/context/errorDetection.js +48 -1
- package/dist/lib/context/errors.d.ts +19 -0
- package/dist/lib/context/errors.js +22 -0
- package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/lib/core/baseProvider.js +306 -200
- package/dist/lib/core/conversationMemoryManager.js +104 -61
- package/dist/lib/core/evaluationProviders.js +16 -33
- package/dist/lib/core/factory.js +237 -164
- package/dist/lib/core/modules/GenerationHandler.js +175 -116
- package/dist/lib/core/modules/MessageBuilder.js +222 -170
- package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
- package/dist/lib/core/modules/StreamHandler.js +95 -27
- package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/lib/core/modules/TelemetryHandler.js +25 -7
- package/dist/lib/core/modules/ToolsManager.js +115 -191
- package/dist/lib/core/redisConversationMemoryManager.js +418 -282
- package/dist/lib/factories/providerRegistry.d.ts +5 -0
- package/dist/lib/factories/providerRegistry.js +20 -2
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.js +4 -2
- package/dist/lib/mcp/externalServerManager.js +66 -0
- package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/lib/mcp/mcpClientFactory.js +16 -0
- package/dist/lib/mcp/toolDiscoveryService.js +32 -6
- package/dist/lib/mcp/toolRegistry.js +193 -123
- package/dist/lib/models/anthropicModels.d.ts +267 -0
- package/dist/lib/models/anthropicModels.js +528 -0
- package/dist/lib/neurolink.d.ts +6 -0
- package/dist/lib/neurolink.js +1162 -646
- package/dist/lib/providers/amazonBedrock.d.ts +1 -1
- package/dist/lib/providers/amazonBedrock.js +521 -319
- package/dist/lib/providers/anthropic.d.ts +123 -2
- package/dist/lib/providers/anthropic.js +873 -27
- package/dist/lib/providers/anthropicBaseProvider.js +77 -17
- package/dist/lib/providers/googleAiStudio.d.ts +1 -1
- package/dist/lib/providers/googleAiStudio.js +292 -227
- package/dist/lib/providers/googleVertex.d.ts +36 -1
- package/dist/lib/providers/googleVertex.js +553 -260
- package/dist/lib/providers/ollama.js +329 -278
- package/dist/lib/providers/openAI.js +77 -19
- package/dist/lib/providers/sagemaker/parsers.js +3 -3
- package/dist/lib/providers/sagemaker/streaming.js +3 -3
- package/dist/lib/proxy/proxyFetch.js +81 -48
- package/dist/lib/rag/ChunkerFactory.js +1 -1
- package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/lib/rag/chunking/markdownChunker.js +174 -2
- package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
- package/dist/lib/rag/ragIntegration.d.ts +18 -1
- package/dist/lib/rag/ragIntegration.js +94 -14
- package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
- package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/lib/telemetry/attributes.d.ts +52 -0
- package/dist/lib/telemetry/attributes.js +61 -0
- package/dist/lib/telemetry/index.d.ts +3 -0
- package/dist/lib/telemetry/index.js +3 -0
- package/dist/lib/telemetry/telemetryService.d.ts +6 -0
- package/dist/lib/telemetry/telemetryService.js +6 -0
- package/dist/lib/telemetry/tracers.d.ts +15 -0
- package/dist/lib/telemetry/tracers.js +17 -0
- package/dist/lib/telemetry/withSpan.d.ts +9 -0
- package/dist/lib/telemetry/withSpan.js +35 -0
- package/dist/lib/types/contextTypes.d.ts +10 -0
- package/dist/lib/types/errors.d.ts +62 -0
- package/dist/lib/types/errors.js +107 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/providers.d.ts +107 -0
- package/dist/lib/types/providers.js +69 -0
- package/dist/lib/types/streamTypes.d.ts +14 -0
- package/dist/lib/types/subscriptionTypes.d.ts +893 -0
- package/dist/lib/types/subscriptionTypes.js +8 -0
- package/dist/lib/utils/conversationMemory.js +121 -82
- package/dist/lib/utils/logger.d.ts +5 -0
- package/dist/lib/utils/logger.js +50 -2
- package/dist/lib/utils/messageBuilder.js +22 -42
- package/dist/lib/utils/modelDetection.js +3 -3
- package/dist/lib/utils/providerConfig.d.ts +167 -0
- package/dist/lib/utils/providerConfig.js +619 -9
- package/dist/lib/utils/providerRetry.d.ts +41 -0
- package/dist/lib/utils/providerRetry.js +114 -0
- package/dist/lib/utils/retryability.d.ts +14 -0
- package/dist/lib/utils/retryability.js +23 -0
- package/dist/lib/utils/sanitizers/svg.js +4 -5
- package/dist/lib/utils/tokenEstimation.d.ts +11 -1
- package/dist/lib/utils/tokenEstimation.js +19 -4
- package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
- package/dist/mcp/externalServerManager.js +66 -0
- package/dist/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/mcp/mcpClientFactory.js +16 -0
- package/dist/mcp/toolDiscoveryService.js +32 -6
- package/dist/mcp/toolRegistry.js +193 -123
- package/dist/models/anthropicModels.d.ts +267 -0
- package/dist/models/anthropicModels.js +527 -0
- package/dist/neurolink.d.ts +6 -0
- package/dist/neurolink.js +1162 -646
- package/dist/providers/amazonBedrock.d.ts +1 -1
- package/dist/providers/amazonBedrock.js +521 -319
- package/dist/providers/anthropic.d.ts +123 -2
- package/dist/providers/anthropic.js +873 -27
- package/dist/providers/anthropicBaseProvider.js +77 -17
- package/dist/providers/googleAiStudio.d.ts +1 -1
- package/dist/providers/googleAiStudio.js +292 -227
- package/dist/providers/googleVertex.d.ts +36 -1
- package/dist/providers/googleVertex.js +553 -260
- package/dist/providers/ollama.js +329 -278
- package/dist/providers/openAI.js +77 -19
- package/dist/providers/sagemaker/parsers.js +3 -3
- package/dist/providers/sagemaker/streaming.js +3 -3
- package/dist/proxy/proxyFetch.js +81 -48
- package/dist/rag/ChunkerFactory.js +1 -1
- package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/rag/chunking/markdownChunker.js +174 -2
- package/dist/rag/pipeline/contextAssembly.js +2 -1
- package/dist/rag/ragIntegration.d.ts +18 -1
- package/dist/rag/ragIntegration.js +94 -14
- package/dist/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/server/abstract/baseServerAdapter.js +4 -1
- package/dist/server/adapters/fastifyAdapter.js +35 -30
- package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/telemetry/attributes.d.ts +52 -0
- package/dist/telemetry/attributes.js +60 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.js +3 -0
- package/dist/telemetry/telemetryService.d.ts +6 -0
- package/dist/telemetry/telemetryService.js +6 -0
- package/dist/telemetry/tracers.d.ts +15 -0
- package/dist/telemetry/tracers.js +16 -0
- package/dist/telemetry/withSpan.d.ts +9 -0
- package/dist/telemetry/withSpan.js +34 -0
- package/dist/types/contextTypes.d.ts +10 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/errors.js +107 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +2 -0
- package/dist/types/providers.d.ts +107 -0
- package/dist/types/providers.js +69 -0
- package/dist/types/streamTypes.d.ts +14 -0
- package/dist/types/subscriptionTypes.d.ts +893 -0
- package/dist/types/subscriptionTypes.js +7 -0
- package/dist/utils/conversationMemory.js +121 -82
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +50 -2
- package/dist/utils/messageBuilder.js +22 -42
- package/dist/utils/modelDetection.js +3 -3
- package/dist/utils/providerConfig.d.ts +167 -0
- package/dist/utils/providerConfig.js +619 -9
- package/dist/utils/providerRetry.d.ts +41 -0
- package/dist/utils/providerRetry.js +113 -0
- package/dist/utils/retryability.d.ts +14 -0
- package/dist/utils/retryability.js +22 -0
- package/dist/utils/sanitizers/svg.js +4 -5
- package/dist/utils/tokenEstimation.d.ts +11 -1
- package/dist/utils/tokenEstimation.js +19 -4
- package/dist/utils/videoAnalysisProcessor.js +7 -3
- package/dist/workflow/config.d.ts +26 -26
- package/package.json +2 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
2
|
import { streamText } from "ai";
|
|
3
|
+
import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
3
4
|
import { AIProviderName } from "../constants/enums.js";
|
|
4
5
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
6
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
@@ -7,6 +8,7 @@ import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
|
7
8
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
8
9
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/errors.js";
|
|
9
10
|
import { logger } from "../utils/logger.js";
|
|
11
|
+
import { calculateCost } from "../utils/pricing.js";
|
|
10
12
|
import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
|
|
11
13
|
import { isZodSchema } from "../utils/schemaConversion.js";
|
|
12
14
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
@@ -17,6 +19,7 @@ const getOpenAIApiKey = () => {
|
|
|
17
19
|
const getOpenAIModel = () => {
|
|
18
20
|
return getProviderModel("OPENAI_MODEL", "gpt-4o");
|
|
19
21
|
};
|
|
22
|
+
const streamTracer = trace.getTracer("neurolink.provider.openai");
|
|
20
23
|
/**
|
|
21
24
|
* OpenAI Provider v2 - BaseProvider Implementation
|
|
22
25
|
* Migrated to use factory pattern with exact Google AI provider pattern
|
|
@@ -274,27 +277,82 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
274
277
|
: "no-tools",
|
|
275
278
|
});
|
|
276
279
|
const model = await this.getAISDKModelWithMiddleware(options); // This is where network connection happens!
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
284
|
-
toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
|
|
285
|
-
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
286
|
-
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
287
|
-
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
288
|
-
logger.info("Tool execution completed", { toolResults, toolCalls });
|
|
289
|
-
// Handle tool execution storage
|
|
290
|
-
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
291
|
-
logger.warn("[OpenAIProvider] Failed to store tool executions", {
|
|
292
|
-
provider: this.providerName,
|
|
293
|
-
error: error instanceof Error ? error.message : String(error),
|
|
294
|
-
});
|
|
295
|
-
});
|
|
280
|
+
// Wrap streamText in an OTel span to capture provider-level latency and token usage
|
|
281
|
+
const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
|
|
282
|
+
kind: SpanKind.CLIENT,
|
|
283
|
+
attributes: {
|
|
284
|
+
"gen_ai.system": "openai",
|
|
285
|
+
"gen_ai.request.model": model.modelId || this.modelName || "unknown",
|
|
296
286
|
},
|
|
297
287
|
});
|
|
288
|
+
let result;
|
|
289
|
+
try {
|
|
290
|
+
result = streamText({
|
|
291
|
+
model,
|
|
292
|
+
messages: messages,
|
|
293
|
+
temperature: options.temperature,
|
|
294
|
+
maxTokens: options.maxTokens, // No default limit - unlimited unless specified
|
|
295
|
+
maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
|
|
296
|
+
tools,
|
|
297
|
+
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
298
|
+
toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
|
|
299
|
+
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
300
|
+
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
301
|
+
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
302
|
+
logger.info("Tool execution completed", {
|
|
303
|
+
toolResults,
|
|
304
|
+
toolCalls,
|
|
305
|
+
});
|
|
306
|
+
// Handle tool execution storage
|
|
307
|
+
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
308
|
+
logger.warn("[OpenAIProvider] Failed to store tool executions", {
|
|
309
|
+
provider: this.providerName,
|
|
310
|
+
error: error instanceof Error ? error.message : String(error),
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
catch (streamError) {
|
|
317
|
+
streamSpan.end();
|
|
318
|
+
throw streamError;
|
|
319
|
+
}
|
|
320
|
+
// Collect token usage and finish reason asynchronously when the stream completes,
|
|
321
|
+
// then end the span. This avoids blocking the stream consumer.
|
|
322
|
+
result.usage
|
|
323
|
+
.then((usage) => {
|
|
324
|
+
streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
|
|
325
|
+
streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
|
|
326
|
+
const cost = calculateCost(this.providerName, this.modelName, {
|
|
327
|
+
input: usage.promptTokens || 0,
|
|
328
|
+
output: usage.completionTokens || 0,
|
|
329
|
+
total: (usage.promptTokens || 0) + (usage.completionTokens || 0),
|
|
330
|
+
});
|
|
331
|
+
if (cost && cost > 0) {
|
|
332
|
+
streamSpan.setAttribute("neurolink.cost", cost);
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
.catch(() => {
|
|
336
|
+
// Usage may not be available if the stream is aborted
|
|
337
|
+
});
|
|
338
|
+
result.finishReason
|
|
339
|
+
.then((reason) => {
|
|
340
|
+
streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
|
|
341
|
+
})
|
|
342
|
+
.catch(() => {
|
|
343
|
+
// Finish reason may not be available if the stream is aborted
|
|
344
|
+
});
|
|
345
|
+
result.text
|
|
346
|
+
.then(() => {
|
|
347
|
+
streamSpan.end();
|
|
348
|
+
})
|
|
349
|
+
.catch((err) => {
|
|
350
|
+
streamSpan.setStatus({
|
|
351
|
+
code: SpanStatusCode.ERROR,
|
|
352
|
+
message: err instanceof Error ? err.message : String(err),
|
|
353
|
+
});
|
|
354
|
+
streamSpan.end();
|
|
355
|
+
});
|
|
298
356
|
timeoutController?.cleanup();
|
|
299
357
|
// Debug the actual result structure
|
|
300
358
|
logger.debug(`OpenAI: streamText result structure:`, {
|
|
@@ -9,6 +9,7 @@ import { createStructuredOutputParser, isStructuredContent, } from "./structured
|
|
|
9
9
|
import { SageMakerError } from "./errors.js";
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
11
11
|
import { randomUUID } from "crypto";
|
|
12
|
+
import { estimateTokens } from "../../utils/tokenEstimation.js";
|
|
12
13
|
/**
|
|
13
14
|
* Constants for JSON parsing and validation
|
|
14
15
|
*/
|
|
@@ -623,9 +624,8 @@ function extractApiErrorMessage(errorData) {
|
|
|
623
624
|
* Utility function to estimate token usage when not provided
|
|
624
625
|
*/
|
|
625
626
|
export function estimateTokenUsage(prompt, completion) {
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
const completionTokens = Math.ceil(completion.length / 4);
|
|
627
|
+
const promptTokens = estimateTokens(prompt, "sagemaker");
|
|
628
|
+
const completionTokens = estimateTokens(completion, "sagemaker");
|
|
629
629
|
return {
|
|
630
630
|
promptTokens,
|
|
631
631
|
completionTokens,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { ReadableStream } from "stream/web";
|
|
8
8
|
import { handleSageMakerError, SageMakerError } from "./errors.js";
|
|
9
9
|
import { logger } from "../../utils/logger.js";
|
|
10
|
+
import { estimateTokens } from "../../utils/tokenEstimation.js";
|
|
10
11
|
import { createSageMakerDetector, } from "./detection.js";
|
|
11
12
|
import { StreamingParserFactory } from "./parsers.js";
|
|
12
13
|
/**
|
|
@@ -320,9 +321,8 @@ export async function createSyntheticStream(text, usage, options = {}) {
|
|
|
320
321
|
* @returns Estimated usage information
|
|
321
322
|
*/
|
|
322
323
|
export function estimateTokenUsage(prompt, completion) {
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
const completionTokens = Math.ceil(completion.length / 4);
|
|
324
|
+
const promptTokens = estimateTokens(prompt, "sagemaker");
|
|
325
|
+
const completionTokens = estimateTokens(completion, "sagemaker");
|
|
326
326
|
return {
|
|
327
327
|
promptTokens,
|
|
328
328
|
completionTokens,
|
|
@@ -4,31 +4,84 @@
|
|
|
4
4
|
* Lightweight implementation extracted from research of major proxy packages
|
|
5
5
|
*/
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
8
|
+
import { tracers } from "../telemetry/tracers.js";
|
|
7
9
|
import { shouldBypassProxy } from "./utils/noProxyUtils.js";
|
|
10
|
+
const fetchTracer = tracers.http;
|
|
11
|
+
/**
|
|
12
|
+
* Extract hostname from a URL string for safe logging (no auth tokens or paths).
|
|
13
|
+
* Returns "[unknown]" if parsing fails.
|
|
14
|
+
*/
|
|
15
|
+
function extractHostname(url) {
|
|
16
|
+
try {
|
|
17
|
+
const urlStr = typeof url === "string"
|
|
18
|
+
? url
|
|
19
|
+
: url instanceof URL
|
|
20
|
+
? url.href
|
|
21
|
+
: url.url;
|
|
22
|
+
const parsed = new URL(urlStr);
|
|
23
|
+
return parsed.hostname;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "[unknown]";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
/**
|
|
9
30
|
* Retry-aware fetch wrapper for transient network errors (ECONNRESET, ETIMEDOUT, socket hang up).
|
|
10
31
|
* Protects all LLM API calls and token refreshes that go through createProxyFetch().
|
|
32
|
+
* Instrumented with OpenTelemetry spans for retry visibility.
|
|
11
33
|
*/
|
|
12
34
|
async function fetchWithRetry(url, init, maxRetries = 3, baseDelay = 500) {
|
|
13
|
-
|
|
35
|
+
const hostname = extractHostname(url);
|
|
36
|
+
return fetchTracer.startActiveSpan("neurolink.http.fetchWithRetry", async (span) => {
|
|
37
|
+
span.setAttribute("http.request.max_retries", maxRetries);
|
|
38
|
+
span.setAttribute("http.request.hostname", hostname);
|
|
39
|
+
span.setAttribute("http.request.method", init?.method || "GET");
|
|
40
|
+
let totalAttempts = 0;
|
|
14
41
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
43
|
+
totalAttempts = attempt + 1;
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(url, init);
|
|
46
|
+
// Record success attributes
|
|
47
|
+
span.setAttribute("http.request.total_attempts", totalAttempts);
|
|
48
|
+
span.setAttribute("http.response.status_code", response.status);
|
|
49
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const err = error;
|
|
54
|
+
const isRetryable = err?.code === "ECONNRESET" ||
|
|
55
|
+
err?.code === "ETIMEDOUT" ||
|
|
56
|
+
err?.message?.includes("socket hang up") ||
|
|
57
|
+
err?.message?.includes("network socket disconnected");
|
|
58
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
59
|
+
// Final failure — record on span and rethrow
|
|
60
|
+
span.setAttribute("http.request.total_attempts", totalAttempts);
|
|
61
|
+
span.setStatus({
|
|
62
|
+
code: SpanStatusCode.ERROR,
|
|
63
|
+
message: err?.message || err?.code || "fetchWithRetry final failure",
|
|
64
|
+
});
|
|
65
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
// Transient error — add retry event and continue loop
|
|
69
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
70
|
+
span.addEvent("http.request.retry", {
|
|
71
|
+
"retry.attempt": attempt + 1,
|
|
72
|
+
"retry.delay_ms": delay,
|
|
73
|
+
"retry.error": (err?.code || err?.message || String(error)).slice(0, 256),
|
|
74
|
+
});
|
|
75
|
+
logger.debug(`[fetchWithRetry] Transient error (${err?.code || err?.message}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
76
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
77
|
+
}
|
|
25
78
|
}
|
|
26
|
-
|
|
27
|
-
logger.debug(`[fetchWithRetry] Transient error (${err?.code || err?.message}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
28
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
79
|
+
throw new Error("fetchWithRetry exhausted"); // unreachable
|
|
29
80
|
}
|
|
30
|
-
|
|
31
|
-
|
|
81
|
+
finally {
|
|
82
|
+
span.end();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
32
85
|
}
|
|
33
86
|
/**
|
|
34
87
|
* Parse request body to readable format for debug logging
|
|
@@ -69,22 +122,6 @@ const SENSITIVE_HEADERS = new Set([
|
|
|
69
122
|
"cookie",
|
|
70
123
|
"set-cookie",
|
|
71
124
|
]);
|
|
72
|
-
/**
|
|
73
|
-
* Extract all headers as plain object with sensitive values redacted
|
|
74
|
-
*/
|
|
75
|
-
function getAllHeaders(headers) {
|
|
76
|
-
if (!headers) {
|
|
77
|
-
return {};
|
|
78
|
-
}
|
|
79
|
-
const entries = headers instanceof Headers
|
|
80
|
-
? [...headers.entries()]
|
|
81
|
-
: Array.isArray(headers)
|
|
82
|
-
? headers
|
|
83
|
-
: Object.entries(headers);
|
|
84
|
-
return Object.fromEntries(entries.map(([key, value]) => SENSITIVE_HEADERS.has(key.toLowerCase())
|
|
85
|
-
? [key, `${value.substring(0, 4)}***`]
|
|
86
|
-
: [key, value]));
|
|
87
|
-
}
|
|
88
125
|
/**
|
|
89
126
|
* Clone response and read body + headers for debug logging
|
|
90
127
|
*/
|
|
@@ -287,13 +324,11 @@ export function createProxyFetch() {
|
|
|
287
324
|
? input.href
|
|
288
325
|
: input.url;
|
|
289
326
|
if (logger.shouldLog("debug")) {
|
|
290
|
-
const {
|
|
327
|
+
const { size: bodySize, type: bodyType } = parseBody(init?.body);
|
|
291
328
|
logger.debug("[Observability] HTTP request to LLM provider", {
|
|
292
329
|
requestId: reqId,
|
|
293
330
|
url,
|
|
294
331
|
method: init?.method || "POST",
|
|
295
|
-
headers: getAllHeaders(init?.headers),
|
|
296
|
-
body: requestBody,
|
|
297
332
|
bodySize,
|
|
298
333
|
bodyType,
|
|
299
334
|
});
|
|
@@ -308,10 +343,10 @@ export function createProxyFetch() {
|
|
|
308
343
|
status: response.status,
|
|
309
344
|
statusText: response.statusText,
|
|
310
345
|
durationMs: Date.now() - startTs,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
bodySize: responseSize,
|
|
346
|
+
contentLength: responseSize,
|
|
347
|
+
hasContent: !!responseBody,
|
|
314
348
|
bodyType: responseType,
|
|
349
|
+
responseHeaders,
|
|
315
350
|
});
|
|
316
351
|
}
|
|
317
352
|
return response;
|
|
@@ -345,13 +380,11 @@ export function createProxyFetch() {
|
|
|
345
380
|
: input.url;
|
|
346
381
|
// Request logging with sensitive header redaction — gated behind debug check
|
|
347
382
|
if (logger.shouldLog("debug")) {
|
|
348
|
-
const {
|
|
383
|
+
const { size: bodySize, type: bodyType } = parseBody(init?.body);
|
|
349
384
|
logger.debug("[Observability] HTTP request to LLM provider", {
|
|
350
385
|
requestId,
|
|
351
386
|
url: targetUrl,
|
|
352
387
|
method: init?.method || "POST",
|
|
353
|
-
headers: getAllHeaders(init?.headers),
|
|
354
|
-
body: requestBody,
|
|
355
388
|
bodySize,
|
|
356
389
|
bodyType,
|
|
357
390
|
});
|
|
@@ -392,7 +425,7 @@ export function createProxyFetch() {
|
|
|
392
425
|
// Create/reuse proxy agent (HTTP/HTTPS/SOCKS)
|
|
393
426
|
const agentCache = globalThis.__NL_PROXY_AGENT_CACHE__ ??
|
|
394
427
|
(globalThis.__NL_PROXY_AGENT_CACHE__ = new Map());
|
|
395
|
-
const cacheKey = maskProxyUrl(proxyUrl) ?? proxyUrl; // credentials
|
|
428
|
+
const cacheKey = maskProxyUrl(proxyUrl) ?? proxyUrl; // mask credentials in cache key
|
|
396
429
|
const dispatcher = agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl));
|
|
397
430
|
agentCache.set(cacheKey, dispatcher);
|
|
398
431
|
logger.debug(`[Proxy Fetch] ✅ ENHANCED PROXY AGENT CREATED`, {
|
|
@@ -431,11 +464,11 @@ export function createProxyFetch() {
|
|
|
431
464
|
status: response?.status,
|
|
432
465
|
statusText: response?.statusText,
|
|
433
466
|
durationMs: Date.now() - requestStartTime,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
bodySize: responseSize,
|
|
467
|
+
contentLength: responseSize,
|
|
468
|
+
hasContent: !!responseBody,
|
|
437
469
|
bodyType: responseType,
|
|
438
470
|
proxied: true,
|
|
471
|
+
responseHeaders,
|
|
439
472
|
});
|
|
440
473
|
}
|
|
441
474
|
logger.debug(`[Proxy Fetch] ENHANCED PROXY SUCCESS`, {
|
|
@@ -481,11 +514,11 @@ export function createProxyFetch() {
|
|
|
481
514
|
status: response.status,
|
|
482
515
|
statusText: response.statusText,
|
|
483
516
|
durationMs: Date.now() - requestStartTime,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
bodySize: responseSize,
|
|
517
|
+
contentLength: responseSize,
|
|
518
|
+
hasContent: !!responseBody,
|
|
487
519
|
bodyType: responseType,
|
|
488
520
|
proxied: false,
|
|
521
|
+
responseHeaders,
|
|
489
522
|
});
|
|
490
523
|
}
|
|
491
524
|
return response;
|
|
@@ -50,7 +50,7 @@ const DEFAULT_CHUNKER_METADATA = {
|
|
|
50
50
|
},
|
|
51
51
|
markdown: {
|
|
52
52
|
description: "Splits markdown content by headers and structural elements",
|
|
53
|
-
defaultConfig: { maxSize: 1000, overlap:
|
|
53
|
+
defaultConfig: { maxSize: 1000, overlap: 50 },
|
|
54
54
|
supportedOptions: ["maxSize", "headerLevels", "splitCodeBlocks"],
|
|
55
55
|
useCases: ["Documentation processing", "README files"],
|
|
56
56
|
aliases: ["md", "markdown-header"],
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Markdown Chunker
|
|
3
3
|
*
|
|
4
4
|
* Splits markdown content by headers and structural elements.
|
|
5
|
+
* Preserves markdown tables by detecting table boundaries and splitting
|
|
6
|
+
* on row boundaries when a table exceeds the max chunk size.
|
|
5
7
|
*/
|
|
6
8
|
import type { Chunk, ChunkerConfig, ChunkingStrategy } from "../types.js";
|
|
7
9
|
import { BaseChunker } from "./BaseChunker.js";
|
|
@@ -12,4 +14,24 @@ export declare class MarkdownChunker extends BaseChunker {
|
|
|
12
14
|
readonly strategy: ChunkingStrategy;
|
|
13
15
|
getDefaultConfig(): ChunkerConfig;
|
|
14
16
|
protected doChunk(content: string, config: ChunkerConfig): Promise<Chunk[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Split content while preserving markdown tables.
|
|
19
|
+
*
|
|
20
|
+
* Strategy:
|
|
21
|
+
* 1. Identify table blocks in the content.
|
|
22
|
+
* 2. Split content into segments: non-table text and table blocks.
|
|
23
|
+
* 3. Non-table text is split using paragraph/sentence boundaries (existing logic).
|
|
24
|
+
* 4. Tables that fit in a chunk are kept intact.
|
|
25
|
+
* 5. Oversized tables are split on row boundaries, repeating the header row.
|
|
26
|
+
*/
|
|
27
|
+
private splitContentTableAware;
|
|
28
|
+
/**
|
|
29
|
+
* Split a table on row boundaries, repeating header + separator in each chunk.
|
|
30
|
+
*/
|
|
31
|
+
private splitTableByRows;
|
|
32
|
+
/**
|
|
33
|
+
* Split non-table text using paragraph and sentence boundaries.
|
|
34
|
+
* This is the original splitContent logic extracted for reuse.
|
|
35
|
+
*/
|
|
36
|
+
private splitPlainContent;
|
|
15
37
|
}
|
|
@@ -2,8 +2,42 @@
|
|
|
2
2
|
* Markdown Chunker
|
|
3
3
|
*
|
|
4
4
|
* Splits markdown content by headers and structural elements.
|
|
5
|
+
* Preserves markdown tables by detecting table boundaries and splitting
|
|
6
|
+
* on row boundaries when a table exceeds the max chunk size.
|
|
5
7
|
*/
|
|
6
8
|
import { BaseChunker, DEFAULT_CHUNKER_CONFIG } from "./BaseChunker.js";
|
|
9
|
+
/** Matches a markdown table separator row like |---|---| or |:--:|---:| */
|
|
10
|
+
const TABLE_SEPARATOR_RE = /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|?\s*$/;
|
|
11
|
+
/** Matches a line that looks like a table row (starts with |) */
|
|
12
|
+
const TABLE_ROW_RE = /^\|.+\|?\s*$/;
|
|
13
|
+
/**
|
|
14
|
+
* Detect contiguous table blocks in text.
|
|
15
|
+
* Returns an array of { start, end } line index ranges (inclusive).
|
|
16
|
+
* A table is a sequence of lines where the second line is a separator.
|
|
17
|
+
*/
|
|
18
|
+
function detectTableRanges(lines) {
|
|
19
|
+
const ranges = [];
|
|
20
|
+
let i = 0;
|
|
21
|
+
while (i < lines.length) {
|
|
22
|
+
// A table needs at least a header row + separator
|
|
23
|
+
if (i + 1 < lines.length &&
|
|
24
|
+
TABLE_ROW_RE.test(lines[i]) &&
|
|
25
|
+
TABLE_SEPARATOR_RE.test(lines[i + 1])) {
|
|
26
|
+
const start = i;
|
|
27
|
+
// Advance past header + separator
|
|
28
|
+
i += 2;
|
|
29
|
+
// Consume remaining data rows
|
|
30
|
+
while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
ranges.push({ start, end: i - 1 });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ranges;
|
|
40
|
+
}
|
|
7
41
|
/**
|
|
8
42
|
* Markdown Chunker
|
|
9
43
|
*/
|
|
@@ -13,7 +47,7 @@ export class MarkdownChunker extends BaseChunker {
|
|
|
13
47
|
return {
|
|
14
48
|
...DEFAULT_CHUNKER_CONFIG,
|
|
15
49
|
maxSize: 1000,
|
|
16
|
-
overlap:
|
|
50
|
+
overlap: 50,
|
|
17
51
|
};
|
|
18
52
|
}
|
|
19
53
|
async doChunk(content, config) {
|
|
@@ -74,16 +108,14 @@ export class MarkdownChunker extends BaseChunker {
|
|
|
74
108
|
if (!fullContent) {
|
|
75
109
|
continue;
|
|
76
110
|
}
|
|
77
|
-
// Split if too large
|
|
111
|
+
// Split if too large — use table-aware splitting
|
|
78
112
|
if (fullContent.length > maxSize) {
|
|
79
|
-
const subChunks = this.
|
|
113
|
+
const subChunks = this.splitContentTableAware(fullContent, maxSize);
|
|
80
114
|
for (const sub of subChunks) {
|
|
81
|
-
const startOffset = content.indexOf(sub
|
|
82
|
-
chunks.push(this.createChunk(sub
|
|
83
|
-
? startOffset + sub.text.length
|
|
84
|
-
: offset + sub.text.length, "unknown", { sectionContext: section.header }));
|
|
115
|
+
const startOffset = content.indexOf(sub, offset);
|
|
116
|
+
chunks.push(this.createChunk(sub, chunks.length, startOffset >= 0 ? startOffset : offset, startOffset >= 0 ? startOffset + sub.length : offset + sub.length, "unknown", { sectionContext: section.header }));
|
|
85
117
|
if (startOffset >= 0) {
|
|
86
|
-
offset = startOffset +
|
|
118
|
+
offset = startOffset + sub.length;
|
|
87
119
|
}
|
|
88
120
|
}
|
|
89
121
|
}
|
|
@@ -93,9 +125,181 @@ export class MarkdownChunker extends BaseChunker {
|
|
|
93
125
|
? startOffset + fullContent.length
|
|
94
126
|
: offset + fullContent.length, "unknown", { sectionContext: section.header }));
|
|
95
127
|
if (startOffset >= 0) {
|
|
96
|
-
offset = startOffset +
|
|
128
|
+
offset = startOffset + fullContent.length;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return chunks;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Split content while preserving markdown tables.
|
|
136
|
+
*
|
|
137
|
+
* Strategy:
|
|
138
|
+
* 1. Identify table blocks in the content.
|
|
139
|
+
* 2. Split content into segments: non-table text and table blocks.
|
|
140
|
+
* 3. Non-table text is split using paragraph/sentence boundaries (existing logic).
|
|
141
|
+
* 4. Tables that fit in a chunk are kept intact.
|
|
142
|
+
* 5. Oversized tables are split on row boundaries, repeating the header row.
|
|
143
|
+
*/
|
|
144
|
+
splitContentTableAware(content, maxSize) {
|
|
145
|
+
const lines = content.split("\n");
|
|
146
|
+
const tableRanges = detectTableRanges(lines);
|
|
147
|
+
// If no tables, fall back to existing splitting logic
|
|
148
|
+
if (tableRanges.length === 0) {
|
|
149
|
+
return this.splitPlainContent(content, maxSize, this.config.overlap ?? 0);
|
|
150
|
+
}
|
|
151
|
+
// Build segments: alternating non-table and table blocks
|
|
152
|
+
const segments = [];
|
|
153
|
+
let lineIdx = 0;
|
|
154
|
+
for (const range of tableRanges) {
|
|
155
|
+
// Non-table text before this table
|
|
156
|
+
if (lineIdx < range.start) {
|
|
157
|
+
const text = lines.slice(lineIdx, range.start).join("\n").trim();
|
|
158
|
+
if (text) {
|
|
159
|
+
segments.push({ text, isTable: false });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// The table itself
|
|
163
|
+
const tableText = lines.slice(range.start, range.end + 1).join("\n");
|
|
164
|
+
segments.push({ text: tableText, isTable: true });
|
|
165
|
+
lineIdx = range.end + 1;
|
|
166
|
+
}
|
|
167
|
+
// Trailing non-table text
|
|
168
|
+
if (lineIdx < lines.length) {
|
|
169
|
+
const text = lines.slice(lineIdx).join("\n").trim();
|
|
170
|
+
if (text) {
|
|
171
|
+
segments.push({ text, isTable: false });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Now produce chunks, trying to pack segments together up to maxSize
|
|
175
|
+
const result = [];
|
|
176
|
+
let current = "";
|
|
177
|
+
for (const seg of segments) {
|
|
178
|
+
if (!seg.isTable) {
|
|
179
|
+
// Non-table text: try to append, split if needed
|
|
180
|
+
const pieces = this.splitPlainContent(seg.text, maxSize, this.config.overlap ?? 0);
|
|
181
|
+
for (const piece of pieces) {
|
|
182
|
+
if (current.length === 0) {
|
|
183
|
+
current = piece;
|
|
184
|
+
}
|
|
185
|
+
else if (current.length + 1 + piece.length <= maxSize) {
|
|
186
|
+
current += "\n" + piece;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
result.push(current);
|
|
190
|
+
current = piece;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Table block
|
|
196
|
+
if (seg.text.length <= maxSize) {
|
|
197
|
+
// Table fits — try to append to current chunk
|
|
198
|
+
if (current.length === 0) {
|
|
199
|
+
current = seg.text;
|
|
200
|
+
}
|
|
201
|
+
else if (current.length + 2 + seg.text.length <= maxSize) {
|
|
202
|
+
current += "\n\n" + seg.text;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
result.push(current);
|
|
206
|
+
current = seg.text;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Oversized table — flush current, then split table on row boundaries
|
|
211
|
+
if (current) {
|
|
212
|
+
result.push(current);
|
|
213
|
+
current = "";
|
|
214
|
+
}
|
|
215
|
+
const tableChunks = this.splitTableByRows(seg.text, maxSize);
|
|
216
|
+
result.push(...tableChunks);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (current) {
|
|
221
|
+
result.push(current);
|
|
222
|
+
}
|
|
223
|
+
return result.length > 0 ? result : [content];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Split a table on row boundaries, repeating header + separator in each chunk.
|
|
227
|
+
*/
|
|
228
|
+
splitTableByRows(tableText, maxSize) {
|
|
229
|
+
const rows = tableText.split("\n");
|
|
230
|
+
if (rows.length < 3) {
|
|
231
|
+
// Not a proper table (need header + separator + at least 1 data row)
|
|
232
|
+
return [tableText];
|
|
233
|
+
}
|
|
234
|
+
const headerRow = rows[0];
|
|
235
|
+
const separatorRow = rows[1];
|
|
236
|
+
const headerBlock = headerRow + "\n" + separatorRow;
|
|
237
|
+
const dataRows = rows.slice(2);
|
|
238
|
+
// If even the header doesn't fit, fall back to size-based split
|
|
239
|
+
if (headerBlock.length > maxSize) {
|
|
240
|
+
return this.splitPlainContent(tableText, maxSize, this.config.overlap ?? 0);
|
|
241
|
+
}
|
|
242
|
+
const chunks = [];
|
|
243
|
+
let currentChunk = headerBlock;
|
|
244
|
+
for (const row of dataRows) {
|
|
245
|
+
// Guard: single row exceeds budget — flush and emit as standalone chunk
|
|
246
|
+
const singleRowChunk = `${headerBlock}\n${row}`;
|
|
247
|
+
if (singleRowChunk.length > maxSize) {
|
|
248
|
+
if (currentChunk.length > headerBlock.length) {
|
|
249
|
+
chunks.push(currentChunk);
|
|
250
|
+
}
|
|
251
|
+
chunks.push(singleRowChunk);
|
|
252
|
+
currentChunk = headerBlock;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const candidate = currentChunk + "\n" + row;
|
|
256
|
+
if (candidate.length <= maxSize) {
|
|
257
|
+
currentChunk = candidate;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Flush current chunk (skip if it only contains the header)
|
|
261
|
+
if (currentChunk.length > headerBlock.length) {
|
|
262
|
+
chunks.push(currentChunk);
|
|
263
|
+
}
|
|
264
|
+
// Start new chunk with header repeated
|
|
265
|
+
currentChunk = headerBlock + "\n" + row;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (currentChunk.length > headerBlock.length) {
|
|
269
|
+
chunks.push(currentChunk);
|
|
270
|
+
}
|
|
271
|
+
return chunks.length > 0 ? chunks : [tableText];
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Split non-table text using paragraph and sentence boundaries.
|
|
275
|
+
* This is the original splitContent logic extracted for reuse.
|
|
276
|
+
*/
|
|
277
|
+
splitPlainContent(content, maxSize, overlap = 0) {
|
|
278
|
+
if (content.length <= maxSize) {
|
|
279
|
+
return [content];
|
|
280
|
+
}
|
|
281
|
+
const chunks = [];
|
|
282
|
+
let start = 0;
|
|
283
|
+
while (start < content.length) {
|
|
284
|
+
let end = Math.min(start + maxSize, content.length);
|
|
285
|
+
if (end < content.length) {
|
|
286
|
+
const searchStart = Math.max(start, end - 200);
|
|
287
|
+
const searchText = content.slice(searchStart, end);
|
|
288
|
+
// Look for paragraph break first
|
|
289
|
+
const paragraphBreak = searchText.lastIndexOf("\n\n");
|
|
290
|
+
if (paragraphBreak > 0) {
|
|
291
|
+
end = searchStart + paragraphBreak;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Look for sentence break
|
|
295
|
+
const sentenceBreak = searchText.search(/[.!?]\s+[A-Z]/);
|
|
296
|
+
if (sentenceBreak > 0) {
|
|
297
|
+
end = searchStart + sentenceBreak + 1;
|
|
298
|
+
}
|
|
97
299
|
}
|
|
98
300
|
}
|
|
301
|
+
chunks.push(content.slice(start, end));
|
|
302
|
+
start = Math.max(start + 1, end - overlap);
|
|
99
303
|
}
|
|
100
304
|
return chunks;
|
|
101
305
|
}
|