@juspay/neurolink 10.6.4 → 10.6.5
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/pricing.d.ts +4 -1
- package/dist/analytics/pricing.js +6 -2
- package/dist/browser/neurolink.min.js +400 -400
- package/dist/core/analytics.js +8 -2
- package/dist/core/baseProvider.js +9 -7
- package/dist/core/modules/GenerationHandler.d.ts +8 -0
- package/dist/core/modules/GenerationHandler.js +28 -38
- package/dist/core/modules/TelemetryHandler.d.ts +3 -9
- package/dist/core/modules/TelemetryHandler.js +17 -12
- package/dist/lib/analytics/pricing.d.ts +4 -1
- package/dist/lib/analytics/pricing.js +6 -2
- package/dist/lib/core/analytics.js +8 -2
- package/dist/lib/core/baseProvider.js +9 -7
- package/dist/lib/core/modules/GenerationHandler.d.ts +8 -0
- package/dist/lib/core/modules/GenerationHandler.js +28 -38
- package/dist/lib/core/modules/TelemetryHandler.d.ts +3 -9
- package/dist/lib/core/modules/TelemetryHandler.js +17 -12
- package/dist/lib/neurolink.js +18 -1
- package/dist/lib/providers/amazonBedrock.js +68 -6
- package/dist/lib/providers/anthropic.js +50 -16
- package/dist/lib/providers/googleAiStudio.js +29 -4
- package/dist/lib/providers/googleNativeGemini3.js +10 -0
- package/dist/lib/providers/googleVertex.js +150 -25
- package/dist/lib/providers/litellm.js +13 -2
- package/dist/lib/providers/openAI.js +13 -2
- package/dist/lib/providers/openaiChatCompletionsBase.js +45 -10
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +2 -14
- package/dist/lib/providers/openaiChatCompletionsClient.js +20 -1
- package/dist/lib/providers/sagemaker/language-model.js +5 -3
- package/dist/lib/providers/sagemaker/parsers.js +18 -9
- package/dist/lib/providers/sagemaker/streaming.d.ts +9 -0
- package/dist/lib/providers/sagemaker/streaming.js +64 -6
- package/dist/lib/types/common.d.ts +3 -0
- package/dist/lib/types/openaiCompatible.d.ts +8 -7
- package/dist/lib/types/providers.d.ts +6 -0
- package/dist/lib/utils/pricing.js +15 -3
- package/dist/lib/utils/tokenUtils.js +38 -3
- package/dist/neurolink.js +18 -1
- package/dist/providers/amazonBedrock.js +68 -6
- package/dist/providers/anthropic.js +50 -16
- package/dist/providers/googleAiStudio.js +29 -4
- package/dist/providers/googleNativeGemini3.js +10 -0
- package/dist/providers/googleVertex.js +150 -25
- package/dist/providers/litellm.js +13 -2
- package/dist/providers/openAI.js +13 -2
- package/dist/providers/openaiChatCompletionsBase.js +45 -10
- package/dist/providers/openaiChatCompletionsClient.d.ts +2 -14
- package/dist/providers/openaiChatCompletionsClient.js +20 -1
- package/dist/providers/sagemaker/language-model.js +5 -3
- package/dist/providers/sagemaker/parsers.js +18 -9
- package/dist/providers/sagemaker/streaming.d.ts +9 -0
- package/dist/providers/sagemaker/streaming.js +64 -6
- package/dist/types/common.d.ts +3 -0
- package/dist/types/openaiCompatible.d.ts +8 -7
- package/dist/types/providers.d.ts +6 -0
- package/dist/utils/pricing.js +15 -3
- package/dist/utils/tokenUtils.js +38 -3
- package/package.json +2 -1
package/dist/core/analytics.js
CHANGED
|
@@ -98,8 +98,14 @@ function estimateCost(provider, model, tokens) {
|
|
|
98
98
|
if (!costInfo) {
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
|
-
// Calculate cost using the configuration system (per-1K-token rates)
|
|
102
|
-
|
|
101
|
+
// Calculate cost using the configuration system (per-1K-token rates).
|
|
102
|
+
// costInfo has no cache tiers, so cache tokens are billed at the input
|
|
103
|
+
// rate — the pre-split total a cache-blind provider would report, never $0.
|
|
104
|
+
const inputCost = ((tokens.input +
|
|
105
|
+
(tokens.cacheReadTokens ?? 0) +
|
|
106
|
+
(tokens.cacheCreationTokens ?? 0)) /
|
|
107
|
+
1000) *
|
|
108
|
+
costInfo.input;
|
|
103
109
|
const outputCost = (tokens.output / 1000) * costInfo.output;
|
|
104
110
|
return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // Round to 6 decimal places
|
|
105
111
|
}
|
|
@@ -27,6 +27,7 @@ import { TelemetryHandler } from "./modules/TelemetryHandler.js";
|
|
|
27
27
|
import { ToolsManager } from "./modules/ToolsManager.js";
|
|
28
28
|
import { Utilities } from "./modules/Utilities.js";
|
|
29
29
|
import { generateText } from "../utils/generation.js";
|
|
30
|
+
import { extractTokenUsage } from "../utils/tokenUtils.js";
|
|
30
31
|
/**
|
|
31
32
|
* Read the consumer-facing lifecycle callbacks buried inside a request's
|
|
32
33
|
* middleware blob. The parameter is `unknown` on purpose: request options
|
|
@@ -1079,12 +1080,7 @@ export class BaseProvider {
|
|
|
1079
1080
|
experimental_telemetry: this.telemetryHandler?.getTelemetryConfig(options, "generate"),
|
|
1080
1081
|
});
|
|
1081
1082
|
formattedContent = formattedResult.text;
|
|
1082
|
-
usage =
|
|
1083
|
-
input: formattedResult.usage?.inputTokens || 0,
|
|
1084
|
-
output: formattedResult.usage?.outputTokens || 0,
|
|
1085
|
-
total: (formattedResult.usage?.inputTokens || 0) +
|
|
1086
|
-
(formattedResult.usage?.outputTokens || 0),
|
|
1087
|
-
};
|
|
1083
|
+
usage = extractTokenUsage(formattedResult.totalUsage ?? formattedResult.usage);
|
|
1088
1084
|
logger.debug("[VideoAnalysis] Claude formatting complete", {
|
|
1089
1085
|
formattedLength: formattedContent.length,
|
|
1090
1086
|
usage,
|
|
@@ -1127,13 +1123,19 @@ export class BaseProvider {
|
|
|
1127
1123
|
this.analyzeAIResponse(generateResult);
|
|
1128
1124
|
this.logGenerationComplete(generateResult);
|
|
1129
1125
|
const responseTime = Date.now() - startTime;
|
|
1130
|
-
await this.recordPerformanceMetrics(generateResult.usage, responseTime);
|
|
1131
1126
|
const { toolsUsed, toolExecutions } = this.extractToolInformation(generateResult);
|
|
1132
1127
|
// Prefer the per-call recorder's real records (params/result/timing per
|
|
1133
1128
|
// execution); fall back to a conversion of the step-extraction entries
|
|
1134
1129
|
// for tools the recorder could not wrap (provider-executed tools).
|
|
1135
1130
|
const toolExecutionRecords = resolveToolExecutionRecords(options, toolExecutions);
|
|
1136
1131
|
let enhancedResult = this.formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutionRecords, options);
|
|
1132
|
+
// Recorded AFTER formatEnhancedResult so telemetry sees the same usage
|
|
1133
|
+
// the caller gets: the cross-step aggregate (totalUsage, not last-step
|
|
1134
|
+
// usage) WITH the providerMetadata cache merge applied — otherwise
|
|
1135
|
+
// providers whose cache data lives only in providerMetadata would have
|
|
1136
|
+
// their cache tokens billed at the full input rate in OTEL metrics,
|
|
1137
|
+
// diverging from analytics.cost.
|
|
1138
|
+
await this.recordPerformanceMetrics(enhancedResult.usage, responseTime);
|
|
1137
1139
|
enhancedResult = await this.synthesizeAIResponseIfNeeded(enhancedResult, options);
|
|
1138
1140
|
const finalResult = await this.enhanceResult(enhancedResult, options, startTime);
|
|
1139
1141
|
return finalResult;
|
|
@@ -68,6 +68,14 @@ export declare class GenerationHandler {
|
|
|
68
68
|
* The AI SDK's LanguageModelUsage only has inputTokens/outputTokens.
|
|
69
69
|
* Cache metrics are surfaced via providerMetadata by provider-specific SDK adapters.
|
|
70
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Set gen_ai usage attributes + cache-aware cost on the span from the
|
|
73
|
+
* CROSS-STEP aggregate (result.totalUsage). result.usage is the LAST step
|
|
74
|
+
* only — using it undercounted every multi-step tool loop, and pricing the
|
|
75
|
+
* raw cache-inclusive inputTokens without the cache fields billed cache
|
|
76
|
+
* reads at the full input rate.
|
|
77
|
+
*/
|
|
78
|
+
private setUsageSpanAttributes;
|
|
71
79
|
private extractCacheMetricsFromProviderMetadata;
|
|
72
80
|
/**
|
|
73
81
|
* Log generation completion information
|
|
@@ -475,18 +475,7 @@ export class GenerationHandler {
|
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
477
|
// Set token usage and completion attributes on span
|
|
478
|
-
|
|
479
|
-
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens || 0);
|
|
480
|
-
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens || 0);
|
|
481
|
-
// Cost on span so users can query "what did this trace cost?"
|
|
482
|
-
const cost = calculateCost(this.providerName, this.modelName, {
|
|
483
|
-
input: result.usage.inputTokens || 0,
|
|
484
|
-
output: result.usage.outputTokens || 0,
|
|
485
|
-
total: (result.usage.inputTokens || 0) +
|
|
486
|
-
(result.usage.outputTokens || 0),
|
|
487
|
-
});
|
|
488
|
-
span.setAttribute("neurolink.cost", cost ?? 0);
|
|
489
|
-
}
|
|
478
|
+
this.setUsageSpanAttributes(span, result);
|
|
490
479
|
if (result.finishReason) {
|
|
491
480
|
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
492
481
|
}
|
|
@@ -559,17 +548,7 @@ export class GenerationHandler {
|
|
|
559
548
|
toolCallsTotal: result.toolCalls?.length || 0,
|
|
560
549
|
responseChars: result.text?.length || 0,
|
|
561
550
|
});
|
|
562
|
-
|
|
563
|
-
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens || 0);
|
|
564
|
-
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens || 0);
|
|
565
|
-
const fallbackCost = calculateCost(this.providerName, this.modelName, {
|
|
566
|
-
input: result.usage.inputTokens || 0,
|
|
567
|
-
output: result.usage.outputTokens || 0,
|
|
568
|
-
total: (result.usage.inputTokens || 0) +
|
|
569
|
-
(result.usage.outputTokens || 0),
|
|
570
|
-
});
|
|
571
|
-
span.setAttribute("neurolink.cost", fallbackCost ?? 0);
|
|
572
|
-
}
|
|
551
|
+
this.setUsageSpanAttributes(span, result);
|
|
573
552
|
if (result.finishReason) {
|
|
574
553
|
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
575
554
|
}
|
|
@@ -606,17 +585,7 @@ export class GenerationHandler {
|
|
|
606
585
|
"retry.strategy": "temperature_omitted",
|
|
607
586
|
});
|
|
608
587
|
span.setAttribute("retry.count", 1);
|
|
609
|
-
|
|
610
|
-
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens || 0);
|
|
611
|
-
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens || 0);
|
|
612
|
-
const noTempCost = calculateCost(this.providerName, this.modelName, {
|
|
613
|
-
input: result.usage.inputTokens || 0,
|
|
614
|
-
output: result.usage.outputTokens || 0,
|
|
615
|
-
total: (result.usage.inputTokens || 0) +
|
|
616
|
-
(result.usage.outputTokens || 0),
|
|
617
|
-
});
|
|
618
|
-
span.setAttribute("neurolink.cost", noTempCost ?? 0);
|
|
619
|
-
}
|
|
588
|
+
this.setUsageSpanAttributes(span, result);
|
|
620
589
|
if (result.finishReason) {
|
|
621
590
|
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
622
591
|
}
|
|
@@ -640,6 +609,26 @@ export class GenerationHandler {
|
|
|
640
609
|
* The AI SDK's LanguageModelUsage only has inputTokens/outputTokens.
|
|
641
610
|
* Cache metrics are surfaced via providerMetadata by provider-specific SDK adapters.
|
|
642
611
|
*/
|
|
612
|
+
/**
|
|
613
|
+
* Set gen_ai usage attributes + cache-aware cost on the span from the
|
|
614
|
+
* CROSS-STEP aggregate (result.totalUsage). result.usage is the LAST step
|
|
615
|
+
* only — using it undercounted every multi-step tool loop, and pricing the
|
|
616
|
+
* raw cache-inclusive inputTokens without the cache fields billed cache
|
|
617
|
+
* reads at the full input rate.
|
|
618
|
+
*/
|
|
619
|
+
setUsageSpanAttributes(span, result) {
|
|
620
|
+
const aggregate = result.totalUsage ?? result.usage;
|
|
621
|
+
if (!aggregate) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
span.setAttribute("gen_ai.usage.input_tokens", aggregate.inputTokens || 0);
|
|
625
|
+
span.setAttribute("gen_ai.usage.output_tokens", aggregate.outputTokens || 0);
|
|
626
|
+
// Cost on span so users can query "what did this trace cost?" —
|
|
627
|
+
// extractTokenUsage rebases input onto the uncached remainder and
|
|
628
|
+
// surfaces the cache fields so calculateCost prices each tier.
|
|
629
|
+
const cost = calculateCost(this.providerName, this.modelName, extractTokenUsage(aggregate));
|
|
630
|
+
span.setAttribute("neurolink.cost", cost ?? 0);
|
|
631
|
+
}
|
|
643
632
|
extractCacheMetricsFromProviderMetadata(generateResult) {
|
|
644
633
|
const providerMeta = generateResult.providerMetadata;
|
|
645
634
|
if (!providerMeta) {
|
|
@@ -841,10 +830,11 @@ export class GenerationHandler {
|
|
|
841
830
|
jsonTruncated = true;
|
|
842
831
|
logger.warn("[GenerationHandler] Structured output truncated by token cap (finishReason=length); increase maxTokens", { provider: this.providerName, model: this.modelName });
|
|
843
832
|
}
|
|
844
|
-
// Extract usage with support for different formats and reasoning tokens
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
833
|
+
// Extract usage with support for different formats and reasoning tokens.
|
|
834
|
+
// totalUsage is the CROSS-STEP aggregate; generateResult.usage is the
|
|
835
|
+
// LAST step only, which silently dropped every prior step of a
|
|
836
|
+
// multi-step tool loop.
|
|
837
|
+
const usage = extractTokenUsage(generateResult.totalUsage ?? generateResult.usage);
|
|
848
838
|
// Merge cache metrics from providerMetadata if not already present in usage
|
|
849
839
|
// The AI SDK's LanguageModelUsage doesn't include cache tokens; they come from
|
|
850
840
|
// provider-specific metadata (e.g. Anthropic's providerMetadata.anthropic)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module core/modules/TelemetryHandler
|
|
15
15
|
*/
|
|
16
16
|
import type { NeuroLink } from "../../neurolink.js";
|
|
17
|
-
import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, TextGenerationOptions } from "../../types/index.js";
|
|
17
|
+
import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
|
|
18
18
|
/**
|
|
19
19
|
* TelemetryHandler class - Handles analytics and telemetry for AI providers
|
|
20
20
|
*/
|
|
@@ -34,10 +34,7 @@ export declare class TelemetryHandler {
|
|
|
34
34
|
/**
|
|
35
35
|
* Record performance metrics for a generation
|
|
36
36
|
*/
|
|
37
|
-
recordPerformanceMetrics(usage:
|
|
38
|
-
inputTokens: number | undefined;
|
|
39
|
-
outputTokens: number | undefined;
|
|
40
|
-
} | undefined, responseTime: number): Promise<void>;
|
|
37
|
+
recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
|
|
41
38
|
/**
|
|
42
39
|
* Calculate actual cost based on token usage and provider configuration.
|
|
43
40
|
*
|
|
@@ -50,10 +47,7 @@ export declare class TelemetryHandler {
|
|
|
50
47
|
* causing a ~1,780x under-estimate when the actual model was Claude Sonnet
|
|
51
48
|
* on Vertex AI ($0.000060 vs $0.106895 for the same request).
|
|
52
49
|
*/
|
|
53
|
-
calculateActualCost(usage:
|
|
54
|
-
inputTokens?: number | undefined;
|
|
55
|
-
outputTokens?: number | undefined;
|
|
56
|
-
}): Promise<number>;
|
|
50
|
+
calculateActualCost(usage: TokenUsage): Promise<number>;
|
|
57
51
|
/**
|
|
58
52
|
* Create telemetry configuration for Vercel AI SDK experimental_telemetry
|
|
59
53
|
* This enables automatic OpenTelemetry tracing when telemetry is enabled
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* @module core/modules/TelemetryHandler
|
|
15
15
|
*/
|
|
16
16
|
import { nanoid } from "nanoid";
|
|
17
|
+
import { extractTokenUsage } from "../../utils/tokenUtils.js";
|
|
17
18
|
import { logger } from "../../utils/logger.js";
|
|
18
19
|
import { recordProviderPerformanceFromMetrics } from "../evaluationProviders.js";
|
|
19
20
|
import { modelConfig } from "../modelConfiguration.js";
|
|
@@ -75,8 +76,12 @@ export class TelemetryHandler {
|
|
|
75
76
|
*/
|
|
76
77
|
async recordPerformanceMetrics(usage, responseTime) {
|
|
77
78
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Normalize first: rebases cache-inclusive ai@6 input onto the uncached
|
|
80
|
+
// remainder and surfaces cache tiers so the cost is priced per tier
|
|
81
|
+
// instead of billing cache reads at the full input rate.
|
|
82
|
+
const tokenUsage = extractTokenUsage(usage);
|
|
83
|
+
const totalTokens = tokenUsage.total;
|
|
84
|
+
const actualCost = await this.calculateActualCost(tokenUsage);
|
|
80
85
|
recordProviderPerformanceFromMetrics(this.providerName, {
|
|
81
86
|
responseTime,
|
|
82
87
|
tokensGenerated: totalTokens,
|
|
@@ -109,25 +114,25 @@ export class TelemetryHandler {
|
|
|
109
114
|
*/
|
|
110
115
|
async calculateActualCost(usage) {
|
|
111
116
|
try {
|
|
112
|
-
const promptTokens = usage?.inputTokens || 0;
|
|
113
|
-
const completionTokens = usage?.outputTokens || 0;
|
|
114
117
|
// Try the per-model pricing table first (includes correct rates for
|
|
115
118
|
// Claude on Vertex, cache token rates, etc.)
|
|
116
119
|
if (hasPricing(this.providerName, this.modelName)) {
|
|
117
|
-
return calculateCost(this.providerName, this.modelName,
|
|
118
|
-
input: promptTokens,
|
|
119
|
-
output: completionTokens,
|
|
120
|
-
total: promptTokens + completionTokens,
|
|
121
|
-
});
|
|
120
|
+
return calculateCost(this.providerName, this.modelName, usage);
|
|
122
121
|
}
|
|
123
122
|
// Fall back to provider-level default cost from configuration system
|
|
124
123
|
const costInfo = modelConfig.getCostInfo(this.providerName, this.modelName);
|
|
125
124
|
if (!costInfo) {
|
|
126
125
|
return 0; // No cost info available
|
|
127
126
|
}
|
|
128
|
-
// Calculate cost per 1K tokens
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
// Calculate cost per 1K tokens. costInfo has no cache tiers, so cache
|
|
128
|
+
// tokens are billed at the input rate — the same total a provider
|
|
129
|
+
// without cache-aware splitting would have reported, never $0.
|
|
130
|
+
const inputCost = ((usage.input +
|
|
131
|
+
(usage.cacheReadTokens ?? 0) +
|
|
132
|
+
(usage.cacheCreationTokens ?? 0)) /
|
|
133
|
+
1000) *
|
|
134
|
+
costInfo.input;
|
|
135
|
+
const outputCost = (usage.output / 1000) * costInfo.output;
|
|
131
136
|
return inputCost + outputCost;
|
|
132
137
|
}
|
|
133
138
|
catch (error) {
|
|
@@ -21,4 +21,7 @@
|
|
|
21
21
|
* cross-provider search via openrouter alias)
|
|
22
22
|
* @returns Total cost in USD
|
|
23
23
|
*/
|
|
24
|
-
export declare function calculateAdvancedCost(model: string | undefined, inputTokens: number, outputTokens: number, provider?: string
|
|
24
|
+
export declare function calculateAdvancedCost(model: string | undefined, inputTokens: number, outputTokens: number, provider?: string, cacheTokens?: {
|
|
25
|
+
cacheReadTokens?: number;
|
|
26
|
+
cacheCreationTokens?: number;
|
|
27
|
+
}): number;
|
|
@@ -22,17 +22,21 @@ import { calculateCost } from "../utils/pricing.js";
|
|
|
22
22
|
* cross-provider search via openrouter alias)
|
|
23
23
|
* @returns Total cost in USD
|
|
24
24
|
*/
|
|
25
|
-
export function calculateAdvancedCost(model, inputTokens, outputTokens, provider) {
|
|
25
|
+
export function calculateAdvancedCost(model, inputTokens, outputTokens, provider, cacheTokens) {
|
|
26
26
|
if (!model) {
|
|
27
27
|
return 0;
|
|
28
28
|
}
|
|
29
29
|
// When provider is unknown, use the openrouter/litellm cross-provider
|
|
30
30
|
// search path in findRates (PROVIDER_ALIASES → __cross_provider__).
|
|
31
31
|
const resolvedProvider = provider && provider.length > 0 ? provider : "openrouter";
|
|
32
|
+
const cacheReadTokens = cacheTokens?.cacheReadTokens ?? 0;
|
|
33
|
+
const cacheCreationTokens = cacheTokens?.cacheCreationTokens ?? 0;
|
|
32
34
|
return calculateCost(resolvedProvider, model, {
|
|
33
35
|
input: inputTokens,
|
|
34
36
|
output: outputTokens,
|
|
35
|
-
total: inputTokens + outputTokens,
|
|
37
|
+
total: inputTokens + cacheReadTokens + cacheCreationTokens + outputTokens,
|
|
38
|
+
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
39
|
+
...(cacheCreationTokens > 0 ? { cacheCreationTokens } : {}),
|
|
36
40
|
});
|
|
37
41
|
}
|
|
38
42
|
//# sourceMappingURL=pricing.js.map
|
|
@@ -98,8 +98,14 @@ function estimateCost(provider, model, tokens) {
|
|
|
98
98
|
if (!costInfo) {
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
|
-
// Calculate cost using the configuration system (per-1K-token rates)
|
|
102
|
-
|
|
101
|
+
// Calculate cost using the configuration system (per-1K-token rates).
|
|
102
|
+
// costInfo has no cache tiers, so cache tokens are billed at the input
|
|
103
|
+
// rate — the pre-split total a cache-blind provider would report, never $0.
|
|
104
|
+
const inputCost = ((tokens.input +
|
|
105
|
+
(tokens.cacheReadTokens ?? 0) +
|
|
106
|
+
(tokens.cacheCreationTokens ?? 0)) /
|
|
107
|
+
1000) *
|
|
108
|
+
costInfo.input;
|
|
103
109
|
const outputCost = (tokens.output / 1000) * costInfo.output;
|
|
104
110
|
return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // Round to 6 decimal places
|
|
105
111
|
}
|
|
@@ -27,6 +27,7 @@ import { TelemetryHandler } from "./modules/TelemetryHandler.js";
|
|
|
27
27
|
import { ToolsManager } from "./modules/ToolsManager.js";
|
|
28
28
|
import { Utilities } from "./modules/Utilities.js";
|
|
29
29
|
import { generateText } from "../utils/generation.js";
|
|
30
|
+
import { extractTokenUsage } from "../utils/tokenUtils.js";
|
|
30
31
|
/**
|
|
31
32
|
* Read the consumer-facing lifecycle callbacks buried inside a request's
|
|
32
33
|
* middleware blob. The parameter is `unknown` on purpose: request options
|
|
@@ -1079,12 +1080,7 @@ export class BaseProvider {
|
|
|
1079
1080
|
experimental_telemetry: this.telemetryHandler?.getTelemetryConfig(options, "generate"),
|
|
1080
1081
|
});
|
|
1081
1082
|
formattedContent = formattedResult.text;
|
|
1082
|
-
usage =
|
|
1083
|
-
input: formattedResult.usage?.inputTokens || 0,
|
|
1084
|
-
output: formattedResult.usage?.outputTokens || 0,
|
|
1085
|
-
total: (formattedResult.usage?.inputTokens || 0) +
|
|
1086
|
-
(formattedResult.usage?.outputTokens || 0),
|
|
1087
|
-
};
|
|
1083
|
+
usage = extractTokenUsage(formattedResult.totalUsage ?? formattedResult.usage);
|
|
1088
1084
|
logger.debug("[VideoAnalysis] Claude formatting complete", {
|
|
1089
1085
|
formattedLength: formattedContent.length,
|
|
1090
1086
|
usage,
|
|
@@ -1127,13 +1123,19 @@ export class BaseProvider {
|
|
|
1127
1123
|
this.analyzeAIResponse(generateResult);
|
|
1128
1124
|
this.logGenerationComplete(generateResult);
|
|
1129
1125
|
const responseTime = Date.now() - startTime;
|
|
1130
|
-
await this.recordPerformanceMetrics(generateResult.usage, responseTime);
|
|
1131
1126
|
const { toolsUsed, toolExecutions } = this.extractToolInformation(generateResult);
|
|
1132
1127
|
// Prefer the per-call recorder's real records (params/result/timing per
|
|
1133
1128
|
// execution); fall back to a conversion of the step-extraction entries
|
|
1134
1129
|
// for tools the recorder could not wrap (provider-executed tools).
|
|
1135
1130
|
const toolExecutionRecords = resolveToolExecutionRecords(options, toolExecutions);
|
|
1136
1131
|
let enhancedResult = this.formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutionRecords, options);
|
|
1132
|
+
// Recorded AFTER formatEnhancedResult so telemetry sees the same usage
|
|
1133
|
+
// the caller gets: the cross-step aggregate (totalUsage, not last-step
|
|
1134
|
+
// usage) WITH the providerMetadata cache merge applied — otherwise
|
|
1135
|
+
// providers whose cache data lives only in providerMetadata would have
|
|
1136
|
+
// their cache tokens billed at the full input rate in OTEL metrics,
|
|
1137
|
+
// diverging from analytics.cost.
|
|
1138
|
+
await this.recordPerformanceMetrics(enhancedResult.usage, responseTime);
|
|
1137
1139
|
enhancedResult = await this.synthesizeAIResponseIfNeeded(enhancedResult, options);
|
|
1138
1140
|
const finalResult = await this.enhanceResult(enhancedResult, options, startTime);
|
|
1139
1141
|
return finalResult;
|
|
@@ -68,6 +68,14 @@ export declare class GenerationHandler {
|
|
|
68
68
|
* The AI SDK's LanguageModelUsage only has inputTokens/outputTokens.
|
|
69
69
|
* Cache metrics are surfaced via providerMetadata by provider-specific SDK adapters.
|
|
70
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Set gen_ai usage attributes + cache-aware cost on the span from the
|
|
73
|
+
* CROSS-STEP aggregate (result.totalUsage). result.usage is the LAST step
|
|
74
|
+
* only — using it undercounted every multi-step tool loop, and pricing the
|
|
75
|
+
* raw cache-inclusive inputTokens without the cache fields billed cache
|
|
76
|
+
* reads at the full input rate.
|
|
77
|
+
*/
|
|
78
|
+
private setUsageSpanAttributes;
|
|
71
79
|
private extractCacheMetricsFromProviderMetadata;
|
|
72
80
|
/**
|
|
73
81
|
* Log generation completion information
|
|
@@ -475,18 +475,7 @@ export class GenerationHandler {
|
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
477
|
// Set token usage and completion attributes on span
|
|
478
|
-
|
|
479
|
-
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens || 0);
|
|
480
|
-
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens || 0);
|
|
481
|
-
// Cost on span so users can query "what did this trace cost?"
|
|
482
|
-
const cost = calculateCost(this.providerName, this.modelName, {
|
|
483
|
-
input: result.usage.inputTokens || 0,
|
|
484
|
-
output: result.usage.outputTokens || 0,
|
|
485
|
-
total: (result.usage.inputTokens || 0) +
|
|
486
|
-
(result.usage.outputTokens || 0),
|
|
487
|
-
});
|
|
488
|
-
span.setAttribute("neurolink.cost", cost ?? 0);
|
|
489
|
-
}
|
|
478
|
+
this.setUsageSpanAttributes(span, result);
|
|
490
479
|
if (result.finishReason) {
|
|
491
480
|
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
492
481
|
}
|
|
@@ -559,17 +548,7 @@ export class GenerationHandler {
|
|
|
559
548
|
toolCallsTotal: result.toolCalls?.length || 0,
|
|
560
549
|
responseChars: result.text?.length || 0,
|
|
561
550
|
});
|
|
562
|
-
|
|
563
|
-
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens || 0);
|
|
564
|
-
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens || 0);
|
|
565
|
-
const fallbackCost = calculateCost(this.providerName, this.modelName, {
|
|
566
|
-
input: result.usage.inputTokens || 0,
|
|
567
|
-
output: result.usage.outputTokens || 0,
|
|
568
|
-
total: (result.usage.inputTokens || 0) +
|
|
569
|
-
(result.usage.outputTokens || 0),
|
|
570
|
-
});
|
|
571
|
-
span.setAttribute("neurolink.cost", fallbackCost ?? 0);
|
|
572
|
-
}
|
|
551
|
+
this.setUsageSpanAttributes(span, result);
|
|
573
552
|
if (result.finishReason) {
|
|
574
553
|
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
575
554
|
}
|
|
@@ -606,17 +585,7 @@ export class GenerationHandler {
|
|
|
606
585
|
"retry.strategy": "temperature_omitted",
|
|
607
586
|
});
|
|
608
587
|
span.setAttribute("retry.count", 1);
|
|
609
|
-
|
|
610
|
-
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens || 0);
|
|
611
|
-
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens || 0);
|
|
612
|
-
const noTempCost = calculateCost(this.providerName, this.modelName, {
|
|
613
|
-
input: result.usage.inputTokens || 0,
|
|
614
|
-
output: result.usage.outputTokens || 0,
|
|
615
|
-
total: (result.usage.inputTokens || 0) +
|
|
616
|
-
(result.usage.outputTokens || 0),
|
|
617
|
-
});
|
|
618
|
-
span.setAttribute("neurolink.cost", noTempCost ?? 0);
|
|
619
|
-
}
|
|
588
|
+
this.setUsageSpanAttributes(span, result);
|
|
620
589
|
if (result.finishReason) {
|
|
621
590
|
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
622
591
|
}
|
|
@@ -640,6 +609,26 @@ export class GenerationHandler {
|
|
|
640
609
|
* The AI SDK's LanguageModelUsage only has inputTokens/outputTokens.
|
|
641
610
|
* Cache metrics are surfaced via providerMetadata by provider-specific SDK adapters.
|
|
642
611
|
*/
|
|
612
|
+
/**
|
|
613
|
+
* Set gen_ai usage attributes + cache-aware cost on the span from the
|
|
614
|
+
* CROSS-STEP aggregate (result.totalUsage). result.usage is the LAST step
|
|
615
|
+
* only — using it undercounted every multi-step tool loop, and pricing the
|
|
616
|
+
* raw cache-inclusive inputTokens without the cache fields billed cache
|
|
617
|
+
* reads at the full input rate.
|
|
618
|
+
*/
|
|
619
|
+
setUsageSpanAttributes(span, result) {
|
|
620
|
+
const aggregate = result.totalUsage ?? result.usage;
|
|
621
|
+
if (!aggregate) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
span.setAttribute("gen_ai.usage.input_tokens", aggregate.inputTokens || 0);
|
|
625
|
+
span.setAttribute("gen_ai.usage.output_tokens", aggregate.outputTokens || 0);
|
|
626
|
+
// Cost on span so users can query "what did this trace cost?" —
|
|
627
|
+
// extractTokenUsage rebases input onto the uncached remainder and
|
|
628
|
+
// surfaces the cache fields so calculateCost prices each tier.
|
|
629
|
+
const cost = calculateCost(this.providerName, this.modelName, extractTokenUsage(aggregate));
|
|
630
|
+
span.setAttribute("neurolink.cost", cost ?? 0);
|
|
631
|
+
}
|
|
643
632
|
extractCacheMetricsFromProviderMetadata(generateResult) {
|
|
644
633
|
const providerMeta = generateResult.providerMetadata;
|
|
645
634
|
if (!providerMeta) {
|
|
@@ -841,10 +830,11 @@ export class GenerationHandler {
|
|
|
841
830
|
jsonTruncated = true;
|
|
842
831
|
logger.warn("[GenerationHandler] Structured output truncated by token cap (finishReason=length); increase maxTokens", { provider: this.providerName, model: this.modelName });
|
|
843
832
|
}
|
|
844
|
-
// Extract usage with support for different formats and reasoning tokens
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
833
|
+
// Extract usage with support for different formats and reasoning tokens.
|
|
834
|
+
// totalUsage is the CROSS-STEP aggregate; generateResult.usage is the
|
|
835
|
+
// LAST step only, which silently dropped every prior step of a
|
|
836
|
+
// multi-step tool loop.
|
|
837
|
+
const usage = extractTokenUsage(generateResult.totalUsage ?? generateResult.usage);
|
|
848
838
|
// Merge cache metrics from providerMetadata if not already present in usage
|
|
849
839
|
// The AI SDK's LanguageModelUsage doesn't include cache tokens; they come from
|
|
850
840
|
// provider-specific metadata (e.g. Anthropic's providerMetadata.anthropic)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module core/modules/TelemetryHandler
|
|
15
15
|
*/
|
|
16
16
|
import type { NeuroLink } from "../../neurolink.js";
|
|
17
|
-
import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, TextGenerationOptions } from "../../types/index.js";
|
|
17
|
+
import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
|
|
18
18
|
/**
|
|
19
19
|
* TelemetryHandler class - Handles analytics and telemetry for AI providers
|
|
20
20
|
*/
|
|
@@ -34,10 +34,7 @@ export declare class TelemetryHandler {
|
|
|
34
34
|
/**
|
|
35
35
|
* Record performance metrics for a generation
|
|
36
36
|
*/
|
|
37
|
-
recordPerformanceMetrics(usage:
|
|
38
|
-
inputTokens: number | undefined;
|
|
39
|
-
outputTokens: number | undefined;
|
|
40
|
-
} | undefined, responseTime: number): Promise<void>;
|
|
37
|
+
recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
|
|
41
38
|
/**
|
|
42
39
|
* Calculate actual cost based on token usage and provider configuration.
|
|
43
40
|
*
|
|
@@ -50,10 +47,7 @@ export declare class TelemetryHandler {
|
|
|
50
47
|
* causing a ~1,780x under-estimate when the actual model was Claude Sonnet
|
|
51
48
|
* on Vertex AI ($0.000060 vs $0.106895 for the same request).
|
|
52
49
|
*/
|
|
53
|
-
calculateActualCost(usage:
|
|
54
|
-
inputTokens?: number | undefined;
|
|
55
|
-
outputTokens?: number | undefined;
|
|
56
|
-
}): Promise<number>;
|
|
50
|
+
calculateActualCost(usage: TokenUsage): Promise<number>;
|
|
57
51
|
/**
|
|
58
52
|
* Create telemetry configuration for Vercel AI SDK experimental_telemetry
|
|
59
53
|
* This enables automatic OpenTelemetry tracing when telemetry is enabled
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* @module core/modules/TelemetryHandler
|
|
15
15
|
*/
|
|
16
16
|
import { nanoid } from "nanoid";
|
|
17
|
+
import { extractTokenUsage } from "../../utils/tokenUtils.js";
|
|
17
18
|
import { logger } from "../../utils/logger.js";
|
|
18
19
|
import { recordProviderPerformanceFromMetrics } from "../evaluationProviders.js";
|
|
19
20
|
import { modelConfig } from "../modelConfiguration.js";
|
|
@@ -75,8 +76,12 @@ export class TelemetryHandler {
|
|
|
75
76
|
*/
|
|
76
77
|
async recordPerformanceMetrics(usage, responseTime) {
|
|
77
78
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Normalize first: rebases cache-inclusive ai@6 input onto the uncached
|
|
80
|
+
// remainder and surfaces cache tiers so the cost is priced per tier
|
|
81
|
+
// instead of billing cache reads at the full input rate.
|
|
82
|
+
const tokenUsage = extractTokenUsage(usage);
|
|
83
|
+
const totalTokens = tokenUsage.total;
|
|
84
|
+
const actualCost = await this.calculateActualCost(tokenUsage);
|
|
80
85
|
recordProviderPerformanceFromMetrics(this.providerName, {
|
|
81
86
|
responseTime,
|
|
82
87
|
tokensGenerated: totalTokens,
|
|
@@ -109,25 +114,25 @@ export class TelemetryHandler {
|
|
|
109
114
|
*/
|
|
110
115
|
async calculateActualCost(usage) {
|
|
111
116
|
try {
|
|
112
|
-
const promptTokens = usage?.inputTokens || 0;
|
|
113
|
-
const completionTokens = usage?.outputTokens || 0;
|
|
114
117
|
// Try the per-model pricing table first (includes correct rates for
|
|
115
118
|
// Claude on Vertex, cache token rates, etc.)
|
|
116
119
|
if (hasPricing(this.providerName, this.modelName)) {
|
|
117
|
-
return calculateCost(this.providerName, this.modelName,
|
|
118
|
-
input: promptTokens,
|
|
119
|
-
output: completionTokens,
|
|
120
|
-
total: promptTokens + completionTokens,
|
|
121
|
-
});
|
|
120
|
+
return calculateCost(this.providerName, this.modelName, usage);
|
|
122
121
|
}
|
|
123
122
|
// Fall back to provider-level default cost from configuration system
|
|
124
123
|
const costInfo = modelConfig.getCostInfo(this.providerName, this.modelName);
|
|
125
124
|
if (!costInfo) {
|
|
126
125
|
return 0; // No cost info available
|
|
127
126
|
}
|
|
128
|
-
// Calculate cost per 1K tokens
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
// Calculate cost per 1K tokens. costInfo has no cache tiers, so cache
|
|
128
|
+
// tokens are billed at the input rate — the same total a provider
|
|
129
|
+
// without cache-aware splitting would have reported, never $0.
|
|
130
|
+
const inputCost = ((usage.input +
|
|
131
|
+
(usage.cacheReadTokens ?? 0) +
|
|
132
|
+
(usage.cacheCreationTokens ?? 0)) /
|
|
133
|
+
1000) *
|
|
134
|
+
costInfo.input;
|
|
135
|
+
const outputCost = (usage.output / 1000) * costInfo.output;
|
|
131
136
|
return inputCost + outputCost;
|
|
132
137
|
}
|
|
133
138
|
catch (error) {
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -2746,7 +2746,10 @@ Current user's request: ${currentInput}`;
|
|
|
2746
2746
|
if (!usage || model === "unknown") {
|
|
2747
2747
|
return undefined;
|
|
2748
2748
|
}
|
|
2749
|
-
const totalCost = calculateAdvancedCost(model, usage.input || 0, usage.output || 0, provider === "unknown" ? undefined : provider
|
|
2749
|
+
const totalCost = calculateAdvancedCost(model, usage.input || 0, usage.output || 0, provider === "unknown" ? undefined : provider, {
|
|
2750
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
2751
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
2752
|
+
});
|
|
2750
2753
|
return totalCost > 0 ? totalCost : undefined;
|
|
2751
2754
|
}
|
|
2752
2755
|
/**
|
|
@@ -4134,6 +4137,20 @@ Current user's request: ${currentInput}`;
|
|
|
4134
4137
|
input: textResult.usage.input || 0,
|
|
4135
4138
|
output: textResult.usage.output || 0,
|
|
4136
4139
|
total: textResult.usage.total || 0,
|
|
4140
|
+
// Optional tiers must be forwarded or they silently vanish at
|
|
4141
|
+
// this DTO boundary (cache-aware cost + savings need them).
|
|
4142
|
+
...(textResult.usage.cacheReadTokens !== undefined && {
|
|
4143
|
+
cacheReadTokens: textResult.usage.cacheReadTokens,
|
|
4144
|
+
}),
|
|
4145
|
+
...(textResult.usage.cacheCreationTokens !== undefined && {
|
|
4146
|
+
cacheCreationTokens: textResult.usage.cacheCreationTokens,
|
|
4147
|
+
}),
|
|
4148
|
+
...(textResult.usage.reasoning !== undefined && {
|
|
4149
|
+
reasoning: textResult.usage.reasoning,
|
|
4150
|
+
}),
|
|
4151
|
+
...(textResult.usage.cacheSavingsPercent !== undefined && {
|
|
4152
|
+
cacheSavingsPercent: textResult.usage.cacheSavingsPercent,
|
|
4153
|
+
}),
|
|
4137
4154
|
}
|
|
4138
4155
|
: undefined,
|
|
4139
4156
|
responseTime: textResult.responseTime,
|