@juspay/neurolink 9.94.7 → 9.95.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 +6 -0
- package/dist/analytics/index.d.ts +7 -0
- package/dist/analytics/index.js +7 -0
- package/dist/analytics/parseQualityScore.d.ts +9 -0
- package/dist/analytics/parseQualityScore.js +29 -0
- package/dist/analytics/pricing.d.ts +24 -0
- package/dist/analytics/pricing.js +37 -0
- package/dist/analytics/service.d.ts +36 -0
- package/dist/analytics/service.js +393 -0
- package/dist/analytics/storage.d.ts +19 -0
- package/dist/analytics/storage.js +32 -0
- package/dist/browser/neurolink.min.js +389 -389
- package/dist/lib/analytics/index.d.ts +7 -0
- package/dist/lib/analytics/index.js +8 -0
- package/dist/lib/analytics/parseQualityScore.d.ts +9 -0
- package/dist/lib/analytics/parseQualityScore.js +30 -0
- package/dist/lib/analytics/pricing.d.ts +24 -0
- package/dist/lib/analytics/pricing.js +38 -0
- package/dist/lib/analytics/service.d.ts +36 -0
- package/dist/lib/analytics/service.js +394 -0
- package/dist/lib/analytics/storage.d.ts +19 -0
- package/dist/lib/analytics/storage.js +33 -0
- package/dist/lib/neurolink.d.ts +54 -0
- package/dist/lib/neurolink.js +302 -204
- package/dist/lib/types/analytics.d.ts +127 -0
- package/dist/neurolink.d.ts +54 -0
- package/dist/neurolink.js +302 -204
- package/dist/types/analytics.d.ts +127 -0
- package/package.json +2 -1
package/dist/neurolink.js
CHANGED
|
@@ -59,6 +59,7 @@ import { buildSkillActivationMessage } from "./skills/skillSessionTracker.js";
|
|
|
59
59
|
import { SkillsManager } from "./skills/skillsManager.js";
|
|
60
60
|
import { createSkillCallTools, createSkillTools } from "./skills/skillTools.js";
|
|
61
61
|
import { getMetricsAggregator, MetricsAggregator, } from "./observability/metricsAggregator.js";
|
|
62
|
+
import { AnalyticsService, calculateAdvancedCost, parseAnalyticsQualityScore, } from "./analytics/index.js";
|
|
62
63
|
import { SpanStatus, SpanType, CircuitBreakerOpenError, ConversationMemoryError, ModelAccessDeniedError, } from "./types/index.js";
|
|
63
64
|
import { SpanSerializer } from "./observability/utils/spanSerializer.js";
|
|
64
65
|
import { flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, stampGuestRescueIdentity, } from "./services/server/ai/observability/instrumentation.js";
|
|
@@ -802,6 +803,7 @@ export class NeuroLink {
|
|
|
802
803
|
*/
|
|
803
804
|
observabilityConfig;
|
|
804
805
|
metricsAggregator = new MetricsAggregator();
|
|
806
|
+
analyticsService;
|
|
805
807
|
/**
|
|
806
808
|
* Per-request metrics trace context backed by AsyncLocalStorage.
|
|
807
809
|
* Safe for concurrent requests on the same SDK instance.
|
|
@@ -814,6 +816,7 @@ export class NeuroLink {
|
|
|
814
816
|
this.toolRegistry = config?.toolRegistry || new MCPToolRegistry();
|
|
815
817
|
this.fileRegistry = new FileReferenceRegistry();
|
|
816
818
|
this.observabilityConfig = config?.observability;
|
|
819
|
+
this.analyticsService = new AnalyticsService();
|
|
817
820
|
// Initialize orchestration setting
|
|
818
821
|
this.enableOrchestration = config?.enableOrchestration ?? false;
|
|
819
822
|
// NL-004: Initialize model alias configuration
|
|
@@ -2564,6 +2567,38 @@ Current user's request: ${currentInput}`;
|
|
|
2564
2567
|
recordMetricsSpan(span) {
|
|
2565
2568
|
this.metricsAggregator.recordSpan(span);
|
|
2566
2569
|
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Get provider metrics analysis
|
|
2572
|
+
* Retrieves aggregated performance, token usage, latency, and success rates per provider.
|
|
2573
|
+
*
|
|
2574
|
+
* @param options - Filtering options
|
|
2575
|
+
* @returns Comprehensive provider metrics result
|
|
2576
|
+
*/
|
|
2577
|
+
async getProviderMetrics(options) {
|
|
2578
|
+
// Propagate unexpected failures — empty datasets are returned by the
|
|
2579
|
+
// service as valid zeroed results; fabricating those here hid real bugs.
|
|
2580
|
+
return this.analyticsService.getProviderMetrics(options);
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Get cost analysis breakdown
|
|
2584
|
+
* Analyzes AI generation costs across requested groups and provides future projections.
|
|
2585
|
+
*
|
|
2586
|
+
* @param options - Cost configuration options
|
|
2587
|
+
* @returns Detailed cost analysis breakdown
|
|
2588
|
+
*/
|
|
2589
|
+
async getCostAnalysis(options) {
|
|
2590
|
+
return this.analyticsService.getCostAnalysis(options);
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Get team-wide usage analytics
|
|
2594
|
+
* Retrieves request counts, unique active users, provider breakdown, and quality scoring.
|
|
2595
|
+
*
|
|
2596
|
+
* @param options - Team query options
|
|
2597
|
+
* @returns Comprehensive team analytics report
|
|
2598
|
+
*/
|
|
2599
|
+
async getTeamAnalytics(options) {
|
|
2600
|
+
return this.analyticsService.getTeamAnalytics(options);
|
|
2601
|
+
}
|
|
2567
2602
|
/**
|
|
2568
2603
|
* Record a memory operation span to both instance and global metrics aggregators.
|
|
2569
2604
|
* This ensures memory spans are visible via sdk.getSpans() and getMetricsAggregator().getSpans().
|
|
@@ -2661,29 +2696,49 @@ Current user's request: ${currentInput}`;
|
|
|
2661
2696
|
}
|
|
2662
2697
|
}
|
|
2663
2698
|
/**
|
|
2664
|
-
*
|
|
2665
|
-
* Listens to generation:end, stream:complete, and tool:end events.
|
|
2699
|
+
* Fire-and-forget analytics tracking with rejection logging.
|
|
2666
2700
|
*/
|
|
2667
|
-
|
|
2668
|
-
this.
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2701
|
+
enqueueAnalyticsTrackRequest(record) {
|
|
2702
|
+
this.analyticsService.trackRequest(record).catch((error) => {
|
|
2703
|
+
logger.debug("[NeuroLink] analytics trackRequest failed", {
|
|
2704
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2705
|
+
});
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Estimate cost using the canonical SDK pricing table (same source as
|
|
2710
|
+
* AnalyticsService.trackRequest) — avoids diverging TokenTracker rates.
|
|
2711
|
+
*/
|
|
2712
|
+
estimateCostFromUsage(provider, model, usage) {
|
|
2713
|
+
if (!usage || model === "unknown") {
|
|
2714
|
+
return undefined;
|
|
2715
|
+
}
|
|
2716
|
+
const totalCost = calculateAdvancedCost(model, usage.input || 0, usage.output || 0, provider === "unknown" ? undefined : provider);
|
|
2717
|
+
return totalCost > 0 ? totalCost : undefined;
|
|
2718
|
+
}
|
|
2719
|
+
/**
|
|
2720
|
+
* Handle generation:end for Pipeline B spans + advanced analytics.
|
|
2721
|
+
* When pipelineAHandled is set, skips Pipeline B spans but still tracks
|
|
2722
|
+
* analytics so successful AI-SDK generate/stream calls are recorded.
|
|
2723
|
+
* Stream analytics are owned here (not stream:complete) to avoid duplicates.
|
|
2724
|
+
*/
|
|
2725
|
+
handleGenerationEndMetrics(data) {
|
|
2726
|
+
const skipPipelineBSpan = data.pipelineAHandled === true;
|
|
2727
|
+
try {
|
|
2728
|
+
const result = data.result;
|
|
2729
|
+
const usage = result?.usage;
|
|
2730
|
+
const analytics = result?.analytics;
|
|
2731
|
+
const provider = data.provider || result?.provider || "unknown";
|
|
2732
|
+
const model = result?.model || data.model || "unknown";
|
|
2733
|
+
const responseTime = data.responseTime || 0;
|
|
2734
|
+
const eventTimestamp = typeof data.timestamp === "number" && Number.isFinite(data.timestamp)
|
|
2735
|
+
? data.timestamp
|
|
2736
|
+
: Date.now();
|
|
2737
|
+
const traceCtx = this._metricsTraceContext;
|
|
2738
|
+
let computedCost = typeof analytics?.cost === "number" && Number.isFinite(analytics.cost)
|
|
2739
|
+
? analytics.cost
|
|
2740
|
+
: undefined;
|
|
2741
|
+
if (!skipPipelineBSpan) {
|
|
2687
2742
|
let span = SpanSerializer.createGenerationSpan({
|
|
2688
2743
|
provider,
|
|
2689
2744
|
model,
|
|
@@ -2693,17 +2748,9 @@ Current user's request: ${currentInput}`;
|
|
|
2693
2748
|
temperature: data.temperature,
|
|
2694
2749
|
maxTokens: data.maxTokens,
|
|
2695
2750
|
});
|
|
2696
|
-
// Link to the OTel parent span; each Pipeline B span keeps its own
|
|
2697
|
-
// unique spanId to comply with OTel/W3C uniqueness requirements.
|
|
2698
2751
|
if (traceCtx) {
|
|
2699
2752
|
span.parentSpanId = traceCtx.parentSpanId;
|
|
2700
2753
|
}
|
|
2701
|
-
// Mark failed generations with ERROR status so metrics count them
|
|
2702
|
-
// correctly. Client aborts (data.aborted === true) are NOT failures —
|
|
2703
|
-
// they are user-initiated cancellations and must not pollute the
|
|
2704
|
-
// failure rate. Map them to WARNING with the canonical
|
|
2705
|
-
// "Generation aborted by client" message (matches the Langfuse
|
|
2706
|
-
// ContextEnricher mapping for outer/internal generation spans).
|
|
2707
2754
|
let spanStatus;
|
|
2708
2755
|
let statusMessage;
|
|
2709
2756
|
if (data.aborted === true) {
|
|
@@ -2719,7 +2766,6 @@ Current user's request: ${currentInput}`;
|
|
|
2719
2766
|
}
|
|
2720
2767
|
span = SpanSerializer.endSpan(span, spanStatus, statusMessage);
|
|
2721
2768
|
span.durationMs = responseTime;
|
|
2722
|
-
// G2 fix: Check finishReason and escalate to WARNING for partial failures
|
|
2723
2769
|
const finishReason = result?.finishReason ??
|
|
2724
2770
|
data.finishReason;
|
|
2725
2771
|
if (finishReason) {
|
|
@@ -2728,7 +2774,6 @@ Current user's request: ${currentInput}`;
|
|
|
2728
2774
|
span = SpanSerializer.endSpan(span, SpanStatus.WARNING, `Generation stopped: finishReason=${finishReason}`);
|
|
2729
2775
|
}
|
|
2730
2776
|
}
|
|
2731
|
-
// G6 fix: Record retry count on Pipeline B span
|
|
2732
2777
|
if (data.retryCount !== undefined) {
|
|
2733
2778
|
span.attributes["gen_ai.retry_count"] = data.retryCount;
|
|
2734
2779
|
}
|
|
@@ -2744,24 +2789,14 @@ Current user's request: ${currentInput}`;
|
|
|
2744
2789
|
totalCost: analytics.cost,
|
|
2745
2790
|
});
|
|
2746
2791
|
}
|
|
2747
|
-
else
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
const outputCost = ((usage.output || 0) / 1_000_000) * pricing.outputPricePerMillion;
|
|
2754
|
-
const totalCost = inputCost + outputCost;
|
|
2755
|
-
if (totalCost > 0) {
|
|
2756
|
-
span = SpanSerializer.enrichWithCost(span, {
|
|
2757
|
-
inputCost,
|
|
2758
|
-
outputCost,
|
|
2759
|
-
totalCost,
|
|
2760
|
-
});
|
|
2761
|
-
}
|
|
2792
|
+
else {
|
|
2793
|
+
const estimated = this.estimateCostFromUsage(provider, model, usage);
|
|
2794
|
+
if (estimated !== undefined) {
|
|
2795
|
+
span = SpanSerializer.enrichWithCost(span, {
|
|
2796
|
+
totalCost: estimated,
|
|
2797
|
+
});
|
|
2762
2798
|
}
|
|
2763
2799
|
}
|
|
2764
|
-
// Record output (truncated for safety)
|
|
2765
2800
|
const content = result?.content || result?.text;
|
|
2766
2801
|
if (content) {
|
|
2767
2802
|
span = SpanSerializer.updateAttributes(span, {
|
|
@@ -2772,174 +2807,218 @@ Current user's request: ${currentInput}`;
|
|
|
2772
2807
|
}
|
|
2773
2808
|
this.metricsAggregator.recordSpan(span);
|
|
2774
2809
|
getMetricsAggregator().recordSpan(span);
|
|
2810
|
+
const spanCost = span.attributes["ai.cost.total"];
|
|
2811
|
+
if (typeof spanCost === "number" && spanCost > 0) {
|
|
2812
|
+
computedCost = spanCost;
|
|
2813
|
+
}
|
|
2814
|
+
this.enqueueAnalyticsTrackRequest({
|
|
2815
|
+
provider,
|
|
2816
|
+
model,
|
|
2817
|
+
userId: data.userId || data.user || undefined,
|
|
2818
|
+
teamId: data.teamId || undefined,
|
|
2819
|
+
department: data.department || undefined,
|
|
2820
|
+
timestamp: eventTimestamp,
|
|
2821
|
+
latency: responseTime,
|
|
2822
|
+
inputTokens: usage?.input || 0,
|
|
2823
|
+
outputTokens: usage?.output || 0,
|
|
2824
|
+
totalTokens: usage?.total || (usage?.input || 0) + (usage?.output || 0),
|
|
2825
|
+
cost: computedCost,
|
|
2826
|
+
isError: span.status === SpanStatus.ERROR,
|
|
2827
|
+
errorMessage: statusMessage,
|
|
2828
|
+
qualityScore: parseAnalyticsQualityScore(data.qualityScore),
|
|
2829
|
+
});
|
|
2775
2830
|
}
|
|
2776
|
-
|
|
2777
|
-
//
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
const metadata = data.metadata;
|
|
2784
|
-
const durationMs = metadata?.durationMs || 0;
|
|
2785
|
-
const chunkCount = metadata?.chunkCount || 0;
|
|
2786
|
-
const totalLength = metadata?.totalLength || 0;
|
|
2787
|
-
const provider = data.provider || "unknown";
|
|
2788
|
-
const model = data.model || "unknown";
|
|
2789
|
-
const traceCtx = this._metricsTraceContext;
|
|
2790
|
-
let span = SpanSerializer.createGenerationSpan({
|
|
2831
|
+
else {
|
|
2832
|
+
// Pipeline A handled the observation — still record analytics so
|
|
2833
|
+
// successful AI-SDK generate()/stream() calls are not silently dropped.
|
|
2834
|
+
if (computedCost === undefined) {
|
|
2835
|
+
computedCost = this.estimateCostFromUsage(provider, model, usage);
|
|
2836
|
+
}
|
|
2837
|
+
this.enqueueAnalyticsTrackRequest({
|
|
2791
2838
|
provider,
|
|
2792
2839
|
model,
|
|
2793
|
-
|
|
2794
|
-
|
|
2840
|
+
userId: data.userId || data.user || undefined,
|
|
2841
|
+
teamId: data.teamId || undefined,
|
|
2842
|
+
department: data.department || undefined,
|
|
2843
|
+
timestamp: eventTimestamp,
|
|
2844
|
+
latency: responseTime,
|
|
2845
|
+
inputTokens: usage?.input || 0,
|
|
2846
|
+
outputTokens: usage?.output || 0,
|
|
2847
|
+
totalTokens: usage?.total || (usage?.input || 0) + (usage?.output || 0),
|
|
2848
|
+
cost: computedCost,
|
|
2849
|
+
isError: data.success === false || Boolean(data.error),
|
|
2850
|
+
errorMessage: data.error ? String(data.error) : undefined,
|
|
2851
|
+
qualityScore: parseAnalyticsQualityScore(data.qualityScore),
|
|
2795
2852
|
});
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
catch {
|
|
2856
|
+
// Non-blocking
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Pipeline B span recording for stream:complete.
|
|
2861
|
+
* Advanced analytics are recorded from generation:end only (avoids duplicates
|
|
2862
|
+
* when both stream:complete and generation:end fire for the same request).
|
|
2863
|
+
*/
|
|
2864
|
+
handleStreamCompleteMetrics(data) {
|
|
2865
|
+
try {
|
|
2866
|
+
const metadata = data.metadata;
|
|
2867
|
+
const durationMs = metadata?.durationMs || 0;
|
|
2868
|
+
const chunkCount = metadata?.chunkCount || 0;
|
|
2869
|
+
const totalLength = metadata?.totalLength || 0;
|
|
2870
|
+
const provider = data.provider || "unknown";
|
|
2871
|
+
const model = data.model || "unknown";
|
|
2872
|
+
const traceCtx = this._metricsTraceContext;
|
|
2873
|
+
let span = SpanSerializer.createGenerationSpan({
|
|
2874
|
+
provider,
|
|
2875
|
+
model,
|
|
2876
|
+
name: `gen_ai.${provider}.stream`,
|
|
2877
|
+
traceId: traceCtx?.traceId,
|
|
2878
|
+
});
|
|
2879
|
+
if (traceCtx) {
|
|
2880
|
+
span.parentSpanId = traceCtx.parentSpanId;
|
|
2881
|
+
}
|
|
2882
|
+
span = SpanSerializer.endSpan(span, SpanStatus.OK);
|
|
2883
|
+
span.durationMs = durationMs;
|
|
2884
|
+
span.attributes["stream.chunk_count"] = chunkCount;
|
|
2885
|
+
span.attributes["stream.content_length"] = totalLength;
|
|
2886
|
+
const streamFinishReason = metadata?.finishReason ??
|
|
2887
|
+
data.finishReason;
|
|
2888
|
+
if (streamFinishReason) {
|
|
2889
|
+
span.attributes["gen_ai.finish_reason"] = streamFinishReason;
|
|
2890
|
+
if (streamFinishReason === "content-filter" ||
|
|
2891
|
+
streamFinishReason === "length") {
|
|
2892
|
+
span = SpanSerializer.endSpan(span, SpanStatus.WARNING, `Stream stopped: finishReason=${streamFinishReason}`);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
if (data.prompt) {
|
|
2896
|
+
const promptStr = String(data.prompt);
|
|
2897
|
+
span = SpanSerializer.updateAttributes(span, {
|
|
2898
|
+
input: promptStr.length > 5000
|
|
2899
|
+
? promptStr.substring(0, 5000) + "...[truncated]"
|
|
2900
|
+
: promptStr,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
const streamContent = data.content;
|
|
2904
|
+
if (streamContent) {
|
|
2905
|
+
span = SpanSerializer.updateAttributes(span, {
|
|
2906
|
+
output: streamContent.length > 5000
|
|
2907
|
+
? streamContent.substring(0, 5000) + "...[truncated]"
|
|
2908
|
+
: streamContent,
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
const usage = metadata?.usage;
|
|
2912
|
+
if (usage) {
|
|
2913
|
+
span = SpanSerializer.enrichWithTokenUsage(span, {
|
|
2914
|
+
promptTokens: usage.input || 0,
|
|
2915
|
+
completionTokens: usage.output || 0,
|
|
2916
|
+
totalTokens: usage.total || (usage.input || 0) + (usage.output || 0),
|
|
2917
|
+
});
|
|
2918
|
+
const estimated = this.estimateCostFromUsage(provider, model, usage);
|
|
2919
|
+
if (estimated !== undefined) {
|
|
2920
|
+
span = SpanSerializer.enrichWithCost(span, { totalCost: estimated });
|
|
2799
2921
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2922
|
+
}
|
|
2923
|
+
this.metricsAggregator.recordSpan(span);
|
|
2924
|
+
getMetricsAggregator().recordSpan(span);
|
|
2925
|
+
}
|
|
2926
|
+
catch {
|
|
2927
|
+
// Non-blocking
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
handleToolEndMetrics(data) {
|
|
2931
|
+
try {
|
|
2932
|
+
const toolName = data.toolName || data.tool || "unknown";
|
|
2933
|
+
const responseTime = data.responseTime || data.duration || 0;
|
|
2934
|
+
const success = data.success !== undefined ? data.success : !data.error;
|
|
2935
|
+
const traceCtx = this._metricsTraceContext;
|
|
2936
|
+
let span = SpanSerializer.createSpan(SpanType.TOOL_CALL, `tool.${toolName}`, {
|
|
2937
|
+
"tool.name": toolName,
|
|
2938
|
+
"tool.success": success,
|
|
2939
|
+
}, traceCtx?.parentSpanId, traceCtx?.traceId);
|
|
2940
|
+
span = SpanSerializer.endSpan(span, success ? SpanStatus.OK : SpanStatus.ERROR);
|
|
2941
|
+
span.durationMs = responseTime;
|
|
2942
|
+
if (!success) {
|
|
2943
|
+
if (data.error) {
|
|
2944
|
+
span.statusMessage = String(data.error);
|
|
2813
2945
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
const promptStr = String(data.prompt);
|
|
2817
|
-
span = SpanSerializer.updateAttributes(span, {
|
|
2818
|
-
input: promptStr.length > 5000
|
|
2819
|
-
? promptStr.substring(0, 5000) + "...[truncated]"
|
|
2820
|
-
: promptStr,
|
|
2821
|
-
});
|
|
2946
|
+
else if (data.result) {
|
|
2947
|
+
span.statusMessage = extractMcpErrorText(data.result);
|
|
2822
2948
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
span =
|
|
2827
|
-
output: streamContent.length > 5000
|
|
2828
|
-
? streamContent.substring(0, 5000) + "...[truncated]"
|
|
2829
|
-
: streamContent,
|
|
2830
|
-
});
|
|
2949
|
+
}
|
|
2950
|
+
if (data.result) {
|
|
2951
|
+
try {
|
|
2952
|
+
span.attributes["tool.result"] = JSON.stringify(data.result).substring(0, 500);
|
|
2831
2953
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
if (usage) {
|
|
2835
|
-
span = SpanSerializer.enrichWithTokenUsage(span, {
|
|
2836
|
-
promptTokens: usage.input || 0,
|
|
2837
|
-
completionTokens: usage.output || 0,
|
|
2838
|
-
totalTokens: usage.total || (usage.input || 0) + (usage.output || 0),
|
|
2839
|
-
});
|
|
2840
|
-
// Compute cost from token usage
|
|
2841
|
-
if (model !== "unknown") {
|
|
2842
|
-
const tokenTracker = this.metricsAggregator.getTokenTracker();
|
|
2843
|
-
const pricing = tokenTracker.getModelPricing(model);
|
|
2844
|
-
if (pricing) {
|
|
2845
|
-
const inputCost = ((usage.input || 0) / 1_000_000) * pricing.inputPricePerMillion;
|
|
2846
|
-
const outputCost = ((usage.output || 0) / 1_000_000) *
|
|
2847
|
-
pricing.outputPricePerMillion;
|
|
2848
|
-
const totalCost = inputCost + outputCost;
|
|
2849
|
-
if (totalCost > 0) {
|
|
2850
|
-
span = SpanSerializer.enrichWithCost(span, {
|
|
2851
|
-
inputCost,
|
|
2852
|
-
outputCost,
|
|
2853
|
-
totalCost,
|
|
2854
|
-
});
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2954
|
+
catch {
|
|
2955
|
+
// Non-blocking
|
|
2858
2956
|
}
|
|
2859
|
-
this.metricsAggregator.recordSpan(span);
|
|
2860
|
-
getMetricsAggregator().recordSpan(span);
|
|
2861
2957
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2958
|
+
this.metricsAggregator.recordSpan(span);
|
|
2959
|
+
getMetricsAggregator().recordSpan(span);
|
|
2960
|
+
}
|
|
2961
|
+
catch {
|
|
2962
|
+
// Non-blocking
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Pipeline B span recording for stream:error.
|
|
2967
|
+
* Analytics for failed streams come from generation:end (success: false).
|
|
2968
|
+
*/
|
|
2969
|
+
handleStreamErrorMetrics(data) {
|
|
2970
|
+
try {
|
|
2971
|
+
const metadata = data.metadata;
|
|
2972
|
+
const durationMs = metadata?.durationMs || 0;
|
|
2973
|
+
const chunkCount = metadata?.chunkCount || 0;
|
|
2974
|
+
const errorName = metadata?.errorName || "UnknownError";
|
|
2975
|
+
const errorMessage = data.content || "Stream error";
|
|
2976
|
+
const provider = data.provider || "unknown";
|
|
2977
|
+
const model = data.model || "unknown";
|
|
2978
|
+
const traceCtx = this._metricsTraceContext;
|
|
2979
|
+
let span = SpanSerializer.createGenerationSpan({
|
|
2980
|
+
provider,
|
|
2981
|
+
model,
|
|
2982
|
+
name: `gen_ai.${provider}.stream.error`,
|
|
2983
|
+
traceId: traceCtx?.traceId,
|
|
2984
|
+
});
|
|
2985
|
+
if (traceCtx) {
|
|
2986
|
+
span.parentSpanId = traceCtx.parentSpanId;
|
|
2864
2987
|
}
|
|
2988
|
+
span = SpanSerializer.endSpan(span, SpanStatus.ERROR);
|
|
2989
|
+
span.durationMs = durationMs;
|
|
2990
|
+
span.statusMessage = `${errorName}: ${errorMessage}`;
|
|
2991
|
+
span.attributes["stream.chunk_count"] = chunkCount;
|
|
2992
|
+
const isAbort = errorName === "AbortError" ||
|
|
2993
|
+
errorMessage.toLowerCase().includes("aborted") ||
|
|
2994
|
+
errorMessage.toLowerCase().includes("abort");
|
|
2995
|
+
span.attributes["error.type"] = isAbort ? "abort" : errorName;
|
|
2996
|
+
if (isAbort) {
|
|
2997
|
+
span.attributes["stream.aborted"] = true;
|
|
2998
|
+
}
|
|
2999
|
+
this.metricsAggregator.recordSpan(span);
|
|
3000
|
+
getMetricsAggregator().recordSpan(span);
|
|
3001
|
+
}
|
|
3002
|
+
catch {
|
|
3003
|
+
// Non-blocking
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Initialize event listeners that feed span data to MetricsAggregator.
|
|
3008
|
+
* Listens to generation:end, stream:complete, and tool:end events.
|
|
3009
|
+
*/
|
|
3010
|
+
initializeMetricsListeners() {
|
|
3011
|
+
this.emitter.on("generation:end", ((...args) => {
|
|
3012
|
+
this.handleGenerationEndMetrics(args[0]);
|
|
3013
|
+
}));
|
|
3014
|
+
this.emitter.on("stream:complete", ((...args) => {
|
|
3015
|
+
this.handleStreamCompleteMetrics(args[0]);
|
|
2865
3016
|
}));
|
|
2866
3017
|
this.emitter.on("tool:end", ((...args) => {
|
|
2867
|
-
|
|
2868
|
-
try {
|
|
2869
|
-
// Handle both event formats: {toolName} (from emitToolEnd) and {tool} (from executeToolInternal)
|
|
2870
|
-
const toolName = data.toolName || data.tool || "unknown";
|
|
2871
|
-
const responseTime = data.responseTime || data.duration || 0;
|
|
2872
|
-
// success is explicit in one format; infer from error presence in the other
|
|
2873
|
-
const success = data.success !== undefined ? data.success : !data.error;
|
|
2874
|
-
const traceCtx = this._metricsTraceContext;
|
|
2875
|
-
let span = SpanSerializer.createSpan(SpanType.TOOL_CALL, `tool.${toolName}`, {
|
|
2876
|
-
"tool.name": toolName,
|
|
2877
|
-
"tool.success": success,
|
|
2878
|
-
}, traceCtx?.parentSpanId, traceCtx?.traceId);
|
|
2879
|
-
span = SpanSerializer.endSpan(span, success ? SpanStatus.OK : SpanStatus.ERROR);
|
|
2880
|
-
span.durationMs = responseTime;
|
|
2881
|
-
if (!success) {
|
|
2882
|
-
if (data.error) {
|
|
2883
|
-
span.statusMessage = String(data.error);
|
|
2884
|
-
}
|
|
2885
|
-
else if (data.result) {
|
|
2886
|
-
span.statusMessage = extractMcpErrorText(data.result);
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
if (data.result) {
|
|
2890
|
-
try {
|
|
2891
|
-
span.attributes["tool.result"] = JSON.stringify(data.result).substring(0, 500);
|
|
2892
|
-
}
|
|
2893
|
-
catch {
|
|
2894
|
-
// Non-blocking
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
this.metricsAggregator.recordSpan(span);
|
|
2898
|
-
getMetricsAggregator().recordSpan(span);
|
|
2899
|
-
}
|
|
2900
|
-
catch {
|
|
2901
|
-
// Non-blocking
|
|
2902
|
-
}
|
|
3018
|
+
this.handleToolEndMetrics(args[0]);
|
|
2903
3019
|
}));
|
|
2904
3020
|
this.emitter.on("stream:error", ((...args) => {
|
|
2905
|
-
|
|
2906
|
-
try {
|
|
2907
|
-
const metadata = data.metadata;
|
|
2908
|
-
const durationMs = metadata?.durationMs || 0;
|
|
2909
|
-
const chunkCount = metadata?.chunkCount || 0;
|
|
2910
|
-
const errorName = metadata?.errorName || "UnknownError";
|
|
2911
|
-
const errorMessage = data.content || "Stream error";
|
|
2912
|
-
const provider = data.provider || "unknown";
|
|
2913
|
-
const model = data.model || "unknown";
|
|
2914
|
-
const traceCtx = this._metricsTraceContext;
|
|
2915
|
-
let span = SpanSerializer.createGenerationSpan({
|
|
2916
|
-
provider,
|
|
2917
|
-
model,
|
|
2918
|
-
name: `gen_ai.${provider}.stream.error`,
|
|
2919
|
-
traceId: traceCtx?.traceId,
|
|
2920
|
-
});
|
|
2921
|
-
// Link to the OTel parent span; keep unique spanId per W3C spec.
|
|
2922
|
-
if (traceCtx) {
|
|
2923
|
-
span.parentSpanId = traceCtx.parentSpanId;
|
|
2924
|
-
}
|
|
2925
|
-
span = SpanSerializer.endSpan(span, SpanStatus.ERROR);
|
|
2926
|
-
span.durationMs = durationMs;
|
|
2927
|
-
span.statusMessage = `${errorName}: ${errorMessage}`;
|
|
2928
|
-
span.attributes["stream.chunk_count"] = chunkCount;
|
|
2929
|
-
// S7 fix: Distinguish aborts from errors
|
|
2930
|
-
const isAbort = errorName === "AbortError" ||
|
|
2931
|
-
errorMessage.toLowerCase().includes("aborted") ||
|
|
2932
|
-
errorMessage.toLowerCase().includes("abort");
|
|
2933
|
-
span.attributes["error.type"] = isAbort ? "abort" : errorName;
|
|
2934
|
-
if (isAbort) {
|
|
2935
|
-
span.attributes["stream.aborted"] = true;
|
|
2936
|
-
}
|
|
2937
|
-
this.metricsAggregator.recordSpan(span);
|
|
2938
|
-
getMetricsAggregator().recordSpan(span);
|
|
2939
|
-
}
|
|
2940
|
-
catch {
|
|
2941
|
-
// Non-blocking
|
|
2942
|
-
}
|
|
3021
|
+
this.handleStreamErrorMetrics(args[0]);
|
|
2943
3022
|
}));
|
|
2944
3023
|
}
|
|
2945
3024
|
/**
|
|
@@ -8251,21 +8330,40 @@ Current user's request: ${currentInput}`;
|
|
|
8251
8330
|
responseTimeMs: Date.now() - startTime,
|
|
8252
8331
|
contentLength: fallbackAccumulatedContent.length,
|
|
8253
8332
|
});
|
|
8254
|
-
// S6 fix: Emit stream:complete after successful fallback so Pipeline B records it
|
|
8333
|
+
// S6 fix: Emit stream:complete after successful fallback so Pipeline B records it.
|
|
8334
|
+
// Also emit generation:end so advanced analytics tracks the successful fallback
|
|
8335
|
+
// (stream:complete no longer records analytics — avoids double-counting elsewhere).
|
|
8255
8336
|
try {
|
|
8337
|
+
const fallbackModel = options.model || "unknown";
|
|
8338
|
+
const fallbackDuration = Date.now() - startTime;
|
|
8256
8339
|
self.emitter.emit("stream:complete", {
|
|
8257
8340
|
content: fallbackAccumulatedContent,
|
|
8258
8341
|
provider: providerName,
|
|
8259
|
-
model:
|
|
8342
|
+
model: fallbackModel,
|
|
8260
8343
|
finishReason: "stop",
|
|
8261
8344
|
metadata: {
|
|
8262
|
-
durationMs:
|
|
8345
|
+
durationMs: fallbackDuration,
|
|
8263
8346
|
chunkCount: 0,
|
|
8264
8347
|
totalLength: fallbackAccumulatedContent.length,
|
|
8265
8348
|
isFallback: true,
|
|
8266
8349
|
finishReason: "stop",
|
|
8267
8350
|
},
|
|
8268
8351
|
});
|
|
8352
|
+
self.emitter.emit("generation:end", {
|
|
8353
|
+
provider: providerName,
|
|
8354
|
+
model: fallbackModel,
|
|
8355
|
+
responseTime: fallbackDuration,
|
|
8356
|
+
timestamp: Date.now(),
|
|
8357
|
+
result: {
|
|
8358
|
+
content: fallbackAccumulatedContent,
|
|
8359
|
+
usage: { input: 0, output: 0, total: 0 },
|
|
8360
|
+
model: fallbackModel,
|
|
8361
|
+
provider: providerName,
|
|
8362
|
+
finishReason: "stop",
|
|
8363
|
+
},
|
|
8364
|
+
success: true,
|
|
8365
|
+
pipelineAHandled: true,
|
|
8366
|
+
});
|
|
8269
8367
|
}
|
|
8270
8368
|
catch {
|
|
8271
8369
|
/* non-blocking */
|