@juspay/neurolink 9.59.0 → 9.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +1023 -1023
- package/dist/lib/neurolink.d.ts +29 -1
- package/dist/lib/neurolink.js +102 -1
- package/dist/lib/providers/googleAiStudio.js +7 -0
- package/dist/lib/providers/googleVertex.js +5 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/streamDedup.d.ts +14 -0
- package/dist/lib/types/streamDedup.js +2 -0
- package/dist/neurolink.d.ts +29 -1
- package/dist/neurolink.js +102 -1
- package/dist/providers/googleAiStudio.js +7 -0
- package/dist/providers/googleVertex.js +5 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/streamDedup.d.ts +14 -0
- package/dist/types/streamDedup.js +1 -0
- package/package.json +1 -1
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -5,13 +5,41 @@
|
|
|
5
5
|
* Enhanced AI provider system with natural MCP tool access.
|
|
6
6
|
* Uses real MCP infrastructure for tool discovery and execution.
|
|
7
7
|
*/
|
|
8
|
-
import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult } from "./types/index.js";
|
|
8
|
+
import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext } from "./types/index.js";
|
|
9
9
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
10
10
|
import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
|
|
11
11
|
import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
12
12
|
import { MCPToolRegistry } from "./mcp/toolRegistry.js";
|
|
13
13
|
import type { DynamicOptions } from "./types/index.js";
|
|
14
14
|
import { TaskManager } from "./tasks/taskManager.js";
|
|
15
|
+
/**
|
|
16
|
+
* Curator P2-4 dedup (concurrency-safe): native providers emit
|
|
17
|
+
* `generation:end` on the shared SDK emitter. We attach a fresh
|
|
18
|
+
* mutable `dedupContext` object directly to the per-call
|
|
19
|
+
* `StreamOptions` (under `_streamDedupContext`) so each stream gets
|
|
20
|
+
* its own instance — concurrent streams have different option objects
|
|
21
|
+
* and therefore different contexts, so they cannot interfere.
|
|
22
|
+
*
|
|
23
|
+
* Native provider emit sites read `options._streamDedupContext` and
|
|
24
|
+
* flip `.providerEmitted = true` before emitting; the orchestration's
|
|
25
|
+
* finally block reads the same closed-over reference and skips its
|
|
26
|
+
* own emit when the flag is set.
|
|
27
|
+
*
|
|
28
|
+
* This avoids the AsyncLocalStorage approach which doesn't reliably
|
|
29
|
+
* propagate through async-generator yield boundaries when iteration
|
|
30
|
+
* happens from outside the original `run()` scope (e.g. when the
|
|
31
|
+
* consumer drives `for await of result.stream` after `sdk.stream(...)`
|
|
32
|
+
* returns).
|
|
33
|
+
*/
|
|
34
|
+
export declare const STREAM_DEDUP_CONTEXT_KEY: "_streamDedupContext";
|
|
35
|
+
/**
|
|
36
|
+
* Native providers call this from their `generation:end` emit sites,
|
|
37
|
+
* passing the same `options` object they received. Safe no-op when
|
|
38
|
+
* the field isn't set.
|
|
39
|
+
*/
|
|
40
|
+
export declare function markStreamProviderEmittedGenerationEnd(options: {
|
|
41
|
+
_streamDedupContext?: StreamGenerationEndContext;
|
|
42
|
+
} | undefined): void;
|
|
15
43
|
export declare class NeuroLink {
|
|
16
44
|
private mcpInitialized;
|
|
17
45
|
private mcpSkipped;
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -297,6 +297,37 @@ function isNonRetryableProviderError(error) {
|
|
|
297
297
|
* same NeuroLink instance would clobber each other's trace context.
|
|
298
298
|
*/
|
|
299
299
|
const metricsTraceContextStorage = new AsyncLocalStorage();
|
|
300
|
+
/**
|
|
301
|
+
* Curator P2-4 dedup (concurrency-safe): native providers emit
|
|
302
|
+
* `generation:end` on the shared SDK emitter. We attach a fresh
|
|
303
|
+
* mutable `dedupContext` object directly to the per-call
|
|
304
|
+
* `StreamOptions` (under `_streamDedupContext`) so each stream gets
|
|
305
|
+
* its own instance — concurrent streams have different option objects
|
|
306
|
+
* and therefore different contexts, so they cannot interfere.
|
|
307
|
+
*
|
|
308
|
+
* Native provider emit sites read `options._streamDedupContext` and
|
|
309
|
+
* flip `.providerEmitted = true` before emitting; the orchestration's
|
|
310
|
+
* finally block reads the same closed-over reference and skips its
|
|
311
|
+
* own emit when the flag is set.
|
|
312
|
+
*
|
|
313
|
+
* This avoids the AsyncLocalStorage approach which doesn't reliably
|
|
314
|
+
* propagate through async-generator yield boundaries when iteration
|
|
315
|
+
* happens from outside the original `run()` scope (e.g. when the
|
|
316
|
+
* consumer drives `for await of result.stream` after `sdk.stream(...)`
|
|
317
|
+
* returns).
|
|
318
|
+
*/
|
|
319
|
+
export const STREAM_DEDUP_CONTEXT_KEY = "_streamDedupContext";
|
|
320
|
+
/**
|
|
321
|
+
* Native providers call this from their `generation:end` emit sites,
|
|
322
|
+
* passing the same `options` object they received. Safe no-op when
|
|
323
|
+
* the field isn't set.
|
|
324
|
+
*/
|
|
325
|
+
export function markStreamProviderEmittedGenerationEnd(options) {
|
|
326
|
+
const ctx = options?._streamDedupContext;
|
|
327
|
+
if (ctx) {
|
|
328
|
+
ctx.providerEmitted = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
300
331
|
export class NeuroLink {
|
|
301
332
|
mcpInitialized = false;
|
|
302
333
|
mcpSkipped = false;
|
|
@@ -4984,8 +5015,23 @@ Current user's request: ${currentInput}`;
|
|
|
4984
5015
|
const streamStartTime = Date.now();
|
|
4985
5016
|
const sessionId = enhancedOptions.context
|
|
4986
5017
|
?.sessionId;
|
|
5018
|
+
// Curator P2-4 dedup (concurrency-safe): native provider stream paths
|
|
5019
|
+
// (Gemini 3 on Vertex / Google AI Studio) emit `generation:end`
|
|
5020
|
+
// themselves. We attach a per-stream mutable flag directly to
|
|
5021
|
+
// `enhancedOptions._streamDedupContext` — native providers receive
|
|
5022
|
+
// these options and flip the flag before their emit; this finally
|
|
5023
|
+
// block reads the same closed-over reference. Concurrent streams
|
|
5024
|
+
// have different option objects so the contexts don't interfere.
|
|
5025
|
+
const dedupContext = {
|
|
5026
|
+
providerEmitted: false,
|
|
5027
|
+
};
|
|
5028
|
+
enhancedOptions._streamDedupContext = dedupContext;
|
|
4987
5029
|
const processedStream = (async function* () {
|
|
4988
5030
|
let streamError;
|
|
5031
|
+
// Curator P2-4: hoist `resolvedUsage` so the finally block can emit a
|
|
5032
|
+
// single `generation:end` event with cost data. Cost listeners
|
|
5033
|
+
// subscribe here; previously the stream path never fired it.
|
|
5034
|
+
let resolvedUsage;
|
|
4989
5035
|
try {
|
|
4990
5036
|
for await (const chunk of mcpStream) {
|
|
4991
5037
|
chunkCount++;
|
|
@@ -5015,7 +5061,7 @@ Current user's request: ${currentInput}`;
|
|
|
5015
5061
|
accumulatedContent += content;
|
|
5016
5062
|
});
|
|
5017
5063
|
}
|
|
5018
|
-
|
|
5064
|
+
resolvedUsage = streamUsage;
|
|
5019
5065
|
if (!resolvedUsage && streamAnalytics) {
|
|
5020
5066
|
try {
|
|
5021
5067
|
const resolved = await Promise.resolve(streamAnalytics);
|
|
@@ -5090,6 +5136,61 @@ Current user's request: ${currentInput}`;
|
|
|
5090
5136
|
guardrailsBlocked: metadata.guardrailsBlocked,
|
|
5091
5137
|
error: metadata.error,
|
|
5092
5138
|
});
|
|
5139
|
+
// Curator P2-4: emit `generation:end` exactly once per stream so
|
|
5140
|
+
// cost listeners receive the same contract as for `generate()`.
|
|
5141
|
+
// The previous implementation only fired `stream:complete`, leaving
|
|
5142
|
+
// any subscriber to `generation:end` with zero events.
|
|
5143
|
+
//
|
|
5144
|
+
// Dedup: native provider stream paths (Gemini 3 on Vertex / Google
|
|
5145
|
+
// AI Studio) already emit `generation:end` themselves so Pipeline B
|
|
5146
|
+
// (Langfuse) records a GENERATION observation. Skip our emit when
|
|
5147
|
+
// they already fired — preserves their Pipeline B observation
|
|
5148
|
+
// source and keeps the "exactly once" contract. Per-stream flag
|
|
5149
|
+
// is concurrency-safe because it's scoped via AsyncLocalStorage.
|
|
5150
|
+
if (!dedupContext.providerEmitted) {
|
|
5151
|
+
try {
|
|
5152
|
+
const finalProvider = metadata.fallbackProvider ?? providerName ?? "unknown";
|
|
5153
|
+
const finalModel = metadata.fallbackModel ??
|
|
5154
|
+
streamModel ??
|
|
5155
|
+
enhancedOptions.model ??
|
|
5156
|
+
"unknown";
|
|
5157
|
+
const finalFinishReason = streamError
|
|
5158
|
+
? "error"
|
|
5159
|
+
: (streamState.finishReason ?? "stop");
|
|
5160
|
+
self.emitter.emit("generation:end", {
|
|
5161
|
+
provider: finalProvider,
|
|
5162
|
+
model: finalModel,
|
|
5163
|
+
responseTime: Date.now() - streamStartTime,
|
|
5164
|
+
toolsUsed: streamState.toolCalls?.map((t) => t.toolName),
|
|
5165
|
+
timestamp: Date.now(),
|
|
5166
|
+
result: {
|
|
5167
|
+
content: accumulatedContent,
|
|
5168
|
+
usage: resolvedUsage,
|
|
5169
|
+
model: finalModel,
|
|
5170
|
+
provider: finalProvider,
|
|
5171
|
+
finishReason: finalFinishReason,
|
|
5172
|
+
},
|
|
5173
|
+
prompt: enhancedOptions.input?.text ||
|
|
5174
|
+
enhancedOptions.prompt,
|
|
5175
|
+
temperature: enhancedOptions.temperature,
|
|
5176
|
+
maxTokens: enhancedOptions.maxTokens,
|
|
5177
|
+
success: !streamError,
|
|
5178
|
+
error: streamError
|
|
5179
|
+
? streamError instanceof Error
|
|
5180
|
+
? streamError.message
|
|
5181
|
+
: String(streamError)
|
|
5182
|
+
: undefined,
|
|
5183
|
+
pipelineAHandled: true,
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
5186
|
+
catch (emitError) {
|
|
5187
|
+
logger.debug("[NeuroLink.stream] generation:end listener threw — ignored", {
|
|
5188
|
+
error: emitError instanceof Error
|
|
5189
|
+
? emitError.message
|
|
5190
|
+
: String(emitError),
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5093
5194
|
self._disableToolCacheForCurrentRequest = false;
|
|
5094
5195
|
cleanupListeners();
|
|
5095
5196
|
streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
|
|
@@ -4,6 +4,7 @@ import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../constants/enum
|
|
|
4
4
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
6
6
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
7
|
+
import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
|
|
7
8
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
8
9
|
import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
|
|
9
10
|
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
@@ -735,6 +736,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
735
736
|
// AI SDK so experimental_telemetry is never injected; we emit manually.
|
|
736
737
|
const nativeStreamEmitter = this.neurolink?.getEventEmitter();
|
|
737
738
|
if (nativeStreamEmitter) {
|
|
739
|
+
// Curator P2-4 dedup: flag the per-stream context attached
|
|
740
|
+
// to options so the orchestration skips its own emit.
|
|
741
|
+
markStreamProviderEmittedGenerationEnd(options);
|
|
738
742
|
nativeStreamEmitter.emit("generation:end", {
|
|
739
743
|
provider: this.providerName,
|
|
740
744
|
responseTime,
|
|
@@ -767,6 +771,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
767
771
|
// Emit failure generation:end so Pipeline B records the failed stream
|
|
768
772
|
const errorEmitter = this.neurolink?.getEventEmitter();
|
|
769
773
|
if (errorEmitter) {
|
|
774
|
+
// Curator P2-4 dedup: flag the per-stream context attached
|
|
775
|
+
// to options so the orchestration skips its own emit.
|
|
776
|
+
markStreamProviderEmittedGenerationEnd(options);
|
|
770
777
|
errorEmitter.emit("generation:end", {
|
|
771
778
|
provider: this.providerName,
|
|
772
779
|
responseTime: Date.now() - startTime,
|
|
@@ -10,6 +10,7 @@ import { ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
|
|
|
10
10
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
11
11
|
import { DEFAULT_MAX_STEPS, GLOBAL_LOCATION_MODELS, } from "../core/constants.js";
|
|
12
12
|
import { ModelConfigurationManager } from "../core/modelConfiguration.js";
|
|
13
|
+
import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
|
|
13
14
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
14
15
|
import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
|
|
15
16
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
@@ -1630,8 +1631,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1630
1631
|
// Emit generation:end so Pipeline B (Langfuse) creates a GENERATION
|
|
1631
1632
|
// observation. The native @google/genai stream path on Vertex bypasses the
|
|
1632
1633
|
// Vercel AI SDK so experimental_telemetry is never injected; we emit manually.
|
|
1634
|
+
// Curator P2-4 dedup: flag the per-stream context attached to options
|
|
1635
|
+
// so the orchestration in `runStandardStreamRequest` knows we already
|
|
1636
|
+
// emitted and skips its own emit (preserving exactly-once).
|
|
1633
1637
|
const vertexStreamEmitter = this.neurolink?.getEventEmitter();
|
|
1634
1638
|
if (vertexStreamEmitter) {
|
|
1639
|
+
markStreamProviderEmittedGenerationEnd(params.options);
|
|
1635
1640
|
vertexStreamEmitter.emit("generation:end", {
|
|
1636
1641
|
provider: this.providerName,
|
|
1637
1642
|
responseTime,
|
package/dist/lib/types/index.js
CHANGED
|
@@ -60,4 +60,6 @@ export * from "./imageGen.js";
|
|
|
60
60
|
export * from "./elicitation.js";
|
|
61
61
|
// Dynamic Arguments types
|
|
62
62
|
export * from "./dynamic.js";
|
|
63
|
+
// Curator P2-4 dedup: per-stream AsyncLocalStorage context
|
|
64
|
+
export * from "./streamDedup.js";
|
|
63
65
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curator P2-4 dedup (concurrency-safe): per-stream context that lets
|
|
3
|
+
* the orchestration's `runStandardStreamRequest` finally block know
|
|
4
|
+
* whether a *native provider* path within THIS stream's async chain
|
|
5
|
+
* already emitted `generation:end`. Native providers (Vertex / Google
|
|
6
|
+
* AI Studio for Gemini 3, etc.) emit on the shared SDK emitter; without
|
|
7
|
+
* scoping, a concurrent unrelated stream's emit on the same NeuroLink
|
|
8
|
+
* instance would suppress the wrong stream's orchestration emit.
|
|
9
|
+
*
|
|
10
|
+
* AsyncLocalStorage scopes each stream's flag to its own async chain.
|
|
11
|
+
*/
|
|
12
|
+
export type StreamGenerationEndContext = {
|
|
13
|
+
providerEmitted: boolean;
|
|
14
|
+
};
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -5,13 +5,41 @@
|
|
|
5
5
|
* Enhanced AI provider system with natural MCP tool access.
|
|
6
6
|
* Uses real MCP infrastructure for tool discovery and execution.
|
|
7
7
|
*/
|
|
8
|
-
import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult } from "./types/index.js";
|
|
8
|
+
import type { CompactionConfig, CompactionResult, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext } from "./types/index.js";
|
|
9
9
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
10
10
|
import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
|
|
11
11
|
import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
12
12
|
import { MCPToolRegistry } from "./mcp/toolRegistry.js";
|
|
13
13
|
import type { DynamicOptions } from "./types/index.js";
|
|
14
14
|
import { TaskManager } from "./tasks/taskManager.js";
|
|
15
|
+
/**
|
|
16
|
+
* Curator P2-4 dedup (concurrency-safe): native providers emit
|
|
17
|
+
* `generation:end` on the shared SDK emitter. We attach a fresh
|
|
18
|
+
* mutable `dedupContext` object directly to the per-call
|
|
19
|
+
* `StreamOptions` (under `_streamDedupContext`) so each stream gets
|
|
20
|
+
* its own instance — concurrent streams have different option objects
|
|
21
|
+
* and therefore different contexts, so they cannot interfere.
|
|
22
|
+
*
|
|
23
|
+
* Native provider emit sites read `options._streamDedupContext` and
|
|
24
|
+
* flip `.providerEmitted = true` before emitting; the orchestration's
|
|
25
|
+
* finally block reads the same closed-over reference and skips its
|
|
26
|
+
* own emit when the flag is set.
|
|
27
|
+
*
|
|
28
|
+
* This avoids the AsyncLocalStorage approach which doesn't reliably
|
|
29
|
+
* propagate through async-generator yield boundaries when iteration
|
|
30
|
+
* happens from outside the original `run()` scope (e.g. when the
|
|
31
|
+
* consumer drives `for await of result.stream` after `sdk.stream(...)`
|
|
32
|
+
* returns).
|
|
33
|
+
*/
|
|
34
|
+
export declare const STREAM_DEDUP_CONTEXT_KEY: "_streamDedupContext";
|
|
35
|
+
/**
|
|
36
|
+
* Native providers call this from their `generation:end` emit sites,
|
|
37
|
+
* passing the same `options` object they received. Safe no-op when
|
|
38
|
+
* the field isn't set.
|
|
39
|
+
*/
|
|
40
|
+
export declare function markStreamProviderEmittedGenerationEnd(options: {
|
|
41
|
+
_streamDedupContext?: StreamGenerationEndContext;
|
|
42
|
+
} | undefined): void;
|
|
15
43
|
export declare class NeuroLink {
|
|
16
44
|
private mcpInitialized;
|
|
17
45
|
private mcpSkipped;
|
package/dist/neurolink.js
CHANGED
|
@@ -297,6 +297,37 @@ function isNonRetryableProviderError(error) {
|
|
|
297
297
|
* same NeuroLink instance would clobber each other's trace context.
|
|
298
298
|
*/
|
|
299
299
|
const metricsTraceContextStorage = new AsyncLocalStorage();
|
|
300
|
+
/**
|
|
301
|
+
* Curator P2-4 dedup (concurrency-safe): native providers emit
|
|
302
|
+
* `generation:end` on the shared SDK emitter. We attach a fresh
|
|
303
|
+
* mutable `dedupContext` object directly to the per-call
|
|
304
|
+
* `StreamOptions` (under `_streamDedupContext`) so each stream gets
|
|
305
|
+
* its own instance — concurrent streams have different option objects
|
|
306
|
+
* and therefore different contexts, so they cannot interfere.
|
|
307
|
+
*
|
|
308
|
+
* Native provider emit sites read `options._streamDedupContext` and
|
|
309
|
+
* flip `.providerEmitted = true` before emitting; the orchestration's
|
|
310
|
+
* finally block reads the same closed-over reference and skips its
|
|
311
|
+
* own emit when the flag is set.
|
|
312
|
+
*
|
|
313
|
+
* This avoids the AsyncLocalStorage approach which doesn't reliably
|
|
314
|
+
* propagate through async-generator yield boundaries when iteration
|
|
315
|
+
* happens from outside the original `run()` scope (e.g. when the
|
|
316
|
+
* consumer drives `for await of result.stream` after `sdk.stream(...)`
|
|
317
|
+
* returns).
|
|
318
|
+
*/
|
|
319
|
+
export const STREAM_DEDUP_CONTEXT_KEY = "_streamDedupContext";
|
|
320
|
+
/**
|
|
321
|
+
* Native providers call this from their `generation:end` emit sites,
|
|
322
|
+
* passing the same `options` object they received. Safe no-op when
|
|
323
|
+
* the field isn't set.
|
|
324
|
+
*/
|
|
325
|
+
export function markStreamProviderEmittedGenerationEnd(options) {
|
|
326
|
+
const ctx = options?._streamDedupContext;
|
|
327
|
+
if (ctx) {
|
|
328
|
+
ctx.providerEmitted = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
300
331
|
export class NeuroLink {
|
|
301
332
|
mcpInitialized = false;
|
|
302
333
|
mcpSkipped = false;
|
|
@@ -4984,8 +5015,23 @@ Current user's request: ${currentInput}`;
|
|
|
4984
5015
|
const streamStartTime = Date.now();
|
|
4985
5016
|
const sessionId = enhancedOptions.context
|
|
4986
5017
|
?.sessionId;
|
|
5018
|
+
// Curator P2-4 dedup (concurrency-safe): native provider stream paths
|
|
5019
|
+
// (Gemini 3 on Vertex / Google AI Studio) emit `generation:end`
|
|
5020
|
+
// themselves. We attach a per-stream mutable flag directly to
|
|
5021
|
+
// `enhancedOptions._streamDedupContext` — native providers receive
|
|
5022
|
+
// these options and flip the flag before their emit; this finally
|
|
5023
|
+
// block reads the same closed-over reference. Concurrent streams
|
|
5024
|
+
// have different option objects so the contexts don't interfere.
|
|
5025
|
+
const dedupContext = {
|
|
5026
|
+
providerEmitted: false,
|
|
5027
|
+
};
|
|
5028
|
+
enhancedOptions._streamDedupContext = dedupContext;
|
|
4987
5029
|
const processedStream = (async function* () {
|
|
4988
5030
|
let streamError;
|
|
5031
|
+
// Curator P2-4: hoist `resolvedUsage` so the finally block can emit a
|
|
5032
|
+
// single `generation:end` event with cost data. Cost listeners
|
|
5033
|
+
// subscribe here; previously the stream path never fired it.
|
|
5034
|
+
let resolvedUsage;
|
|
4989
5035
|
try {
|
|
4990
5036
|
for await (const chunk of mcpStream) {
|
|
4991
5037
|
chunkCount++;
|
|
@@ -5015,7 +5061,7 @@ Current user's request: ${currentInput}`;
|
|
|
5015
5061
|
accumulatedContent += content;
|
|
5016
5062
|
});
|
|
5017
5063
|
}
|
|
5018
|
-
|
|
5064
|
+
resolvedUsage = streamUsage;
|
|
5019
5065
|
if (!resolvedUsage && streamAnalytics) {
|
|
5020
5066
|
try {
|
|
5021
5067
|
const resolved = await Promise.resolve(streamAnalytics);
|
|
@@ -5090,6 +5136,61 @@ Current user's request: ${currentInput}`;
|
|
|
5090
5136
|
guardrailsBlocked: metadata.guardrailsBlocked,
|
|
5091
5137
|
error: metadata.error,
|
|
5092
5138
|
});
|
|
5139
|
+
// Curator P2-4: emit `generation:end` exactly once per stream so
|
|
5140
|
+
// cost listeners receive the same contract as for `generate()`.
|
|
5141
|
+
// The previous implementation only fired `stream:complete`, leaving
|
|
5142
|
+
// any subscriber to `generation:end` with zero events.
|
|
5143
|
+
//
|
|
5144
|
+
// Dedup: native provider stream paths (Gemini 3 on Vertex / Google
|
|
5145
|
+
// AI Studio) already emit `generation:end` themselves so Pipeline B
|
|
5146
|
+
// (Langfuse) records a GENERATION observation. Skip our emit when
|
|
5147
|
+
// they already fired — preserves their Pipeline B observation
|
|
5148
|
+
// source and keeps the "exactly once" contract. Per-stream flag
|
|
5149
|
+
// is concurrency-safe because it's scoped via AsyncLocalStorage.
|
|
5150
|
+
if (!dedupContext.providerEmitted) {
|
|
5151
|
+
try {
|
|
5152
|
+
const finalProvider = metadata.fallbackProvider ?? providerName ?? "unknown";
|
|
5153
|
+
const finalModel = metadata.fallbackModel ??
|
|
5154
|
+
streamModel ??
|
|
5155
|
+
enhancedOptions.model ??
|
|
5156
|
+
"unknown";
|
|
5157
|
+
const finalFinishReason = streamError
|
|
5158
|
+
? "error"
|
|
5159
|
+
: (streamState.finishReason ?? "stop");
|
|
5160
|
+
self.emitter.emit("generation:end", {
|
|
5161
|
+
provider: finalProvider,
|
|
5162
|
+
model: finalModel,
|
|
5163
|
+
responseTime: Date.now() - streamStartTime,
|
|
5164
|
+
toolsUsed: streamState.toolCalls?.map((t) => t.toolName),
|
|
5165
|
+
timestamp: Date.now(),
|
|
5166
|
+
result: {
|
|
5167
|
+
content: accumulatedContent,
|
|
5168
|
+
usage: resolvedUsage,
|
|
5169
|
+
model: finalModel,
|
|
5170
|
+
provider: finalProvider,
|
|
5171
|
+
finishReason: finalFinishReason,
|
|
5172
|
+
},
|
|
5173
|
+
prompt: enhancedOptions.input?.text ||
|
|
5174
|
+
enhancedOptions.prompt,
|
|
5175
|
+
temperature: enhancedOptions.temperature,
|
|
5176
|
+
maxTokens: enhancedOptions.maxTokens,
|
|
5177
|
+
success: !streamError,
|
|
5178
|
+
error: streamError
|
|
5179
|
+
? streamError instanceof Error
|
|
5180
|
+
? streamError.message
|
|
5181
|
+
: String(streamError)
|
|
5182
|
+
: undefined,
|
|
5183
|
+
pipelineAHandled: true,
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
5186
|
+
catch (emitError) {
|
|
5187
|
+
logger.debug("[NeuroLink.stream] generation:end listener threw — ignored", {
|
|
5188
|
+
error: emitError instanceof Error
|
|
5189
|
+
? emitError.message
|
|
5190
|
+
: String(emitError),
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5093
5194
|
self._disableToolCacheForCurrentRequest = false;
|
|
5094
5195
|
cleanupListeners();
|
|
5095
5196
|
streamSpan.setAttribute("neurolink.response_time_ms", Date.now() - spanStartTime);
|
|
@@ -4,6 +4,7 @@ import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../constants/enum
|
|
|
4
4
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
6
6
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
7
|
+
import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
|
|
7
8
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
8
9
|
import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
|
|
9
10
|
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
@@ -735,6 +736,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
735
736
|
// AI SDK so experimental_telemetry is never injected; we emit manually.
|
|
736
737
|
const nativeStreamEmitter = this.neurolink?.getEventEmitter();
|
|
737
738
|
if (nativeStreamEmitter) {
|
|
739
|
+
// Curator P2-4 dedup: flag the per-stream context attached
|
|
740
|
+
// to options so the orchestration skips its own emit.
|
|
741
|
+
markStreamProviderEmittedGenerationEnd(options);
|
|
738
742
|
nativeStreamEmitter.emit("generation:end", {
|
|
739
743
|
provider: this.providerName,
|
|
740
744
|
responseTime,
|
|
@@ -767,6 +771,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
767
771
|
// Emit failure generation:end so Pipeline B records the failed stream
|
|
768
772
|
const errorEmitter = this.neurolink?.getEventEmitter();
|
|
769
773
|
if (errorEmitter) {
|
|
774
|
+
// Curator P2-4 dedup: flag the per-stream context attached
|
|
775
|
+
// to options so the orchestration skips its own emit.
|
|
776
|
+
markStreamProviderEmittedGenerationEnd(options);
|
|
770
777
|
errorEmitter.emit("generation:end", {
|
|
771
778
|
provider: this.providerName,
|
|
772
779
|
responseTime: Date.now() - startTime,
|
|
@@ -10,6 +10,7 @@ import { ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
|
|
|
10
10
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
11
11
|
import { DEFAULT_MAX_STEPS, GLOBAL_LOCATION_MODELS, } from "../core/constants.js";
|
|
12
12
|
import { ModelConfigurationManager } from "../core/modelConfiguration.js";
|
|
13
|
+
import { markStreamProviderEmittedGenerationEnd, } from "../neurolink.js";
|
|
13
14
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
14
15
|
import { ATTR, tracers, withClientSpan } from "../telemetry/index.js";
|
|
15
16
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
@@ -1630,8 +1631,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1630
1631
|
// Emit generation:end so Pipeline B (Langfuse) creates a GENERATION
|
|
1631
1632
|
// observation. The native @google/genai stream path on Vertex bypasses the
|
|
1632
1633
|
// Vercel AI SDK so experimental_telemetry is never injected; we emit manually.
|
|
1634
|
+
// Curator P2-4 dedup: flag the per-stream context attached to options
|
|
1635
|
+
// so the orchestration in `runStandardStreamRequest` knows we already
|
|
1636
|
+
// emitted and skips its own emit (preserving exactly-once).
|
|
1633
1637
|
const vertexStreamEmitter = this.neurolink?.getEventEmitter();
|
|
1634
1638
|
if (vertexStreamEmitter) {
|
|
1639
|
+
markStreamProviderEmittedGenerationEnd(params.options);
|
|
1635
1640
|
vertexStreamEmitter.emit("generation:end", {
|
|
1636
1641
|
provider: this.providerName,
|
|
1637
1642
|
responseTime,
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curator P2-4 dedup (concurrency-safe): per-stream context that lets
|
|
3
|
+
* the orchestration's `runStandardStreamRequest` finally block know
|
|
4
|
+
* whether a *native provider* path within THIS stream's async chain
|
|
5
|
+
* already emitted `generation:end`. Native providers (Vertex / Google
|
|
6
|
+
* AI Studio for Gemini 3, etc.) emit on the shared SDK emitter; without
|
|
7
|
+
* scoping, a concurrent unrelated stream's emit on the same NeuroLink
|
|
8
|
+
* instance would suppress the wrong stream's orchestration emit.
|
|
9
|
+
*
|
|
10
|
+
* AsyncLocalStorage scopes each stream's flag to its own async chain.
|
|
11
|
+
*/
|
|
12
|
+
export type StreamGenerationEndContext = {
|
|
13
|
+
providerEmitted: boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.59.
|
|
3
|
+
"version": "9.59.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
6
6
|
"author": {
|