@juspay/neurolink 10.6.3 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/analytics/pricing.d.ts +4 -1
  3. package/dist/analytics/pricing.js +6 -2
  4. package/dist/browser/neurolink.min.js +400 -400
  5. package/dist/core/analytics.js +8 -2
  6. package/dist/core/baseProvider.js +9 -7
  7. package/dist/core/modules/GenerationHandler.d.ts +8 -0
  8. package/dist/core/modules/GenerationHandler.js +28 -38
  9. package/dist/core/modules/TelemetryHandler.d.ts +3 -9
  10. package/dist/core/modules/TelemetryHandler.js +17 -12
  11. package/dist/lib/analytics/pricing.d.ts +4 -1
  12. package/dist/lib/analytics/pricing.js +6 -2
  13. package/dist/lib/core/analytics.js +8 -2
  14. package/dist/lib/core/baseProvider.js +9 -7
  15. package/dist/lib/core/modules/GenerationHandler.d.ts +8 -0
  16. package/dist/lib/core/modules/GenerationHandler.js +28 -38
  17. package/dist/lib/core/modules/TelemetryHandler.d.ts +3 -9
  18. package/dist/lib/core/modules/TelemetryHandler.js +17 -12
  19. package/dist/lib/neurolink.js +18 -1
  20. package/dist/lib/processors/media/index.d.ts +1 -1
  21. package/dist/lib/processors/media/index.js +1 -1
  22. package/dist/lib/providers/amazonBedrock.js +68 -6
  23. package/dist/lib/providers/anthropic.js +50 -16
  24. package/dist/lib/providers/googleAiStudio.js +29 -4
  25. package/dist/lib/providers/googleNativeGemini3.js +10 -0
  26. package/dist/lib/providers/googleVertex.js +150 -25
  27. package/dist/lib/providers/litellm.js +13 -2
  28. package/dist/lib/providers/openAI.js +13 -2
  29. package/dist/lib/providers/openaiChatCompletionsBase.js +45 -10
  30. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +2 -14
  31. package/dist/lib/providers/openaiChatCompletionsClient.js +20 -1
  32. package/dist/lib/providers/sagemaker/language-model.js +5 -3
  33. package/dist/lib/providers/sagemaker/parsers.js +18 -9
  34. package/dist/lib/providers/sagemaker/streaming.d.ts +9 -0
  35. package/dist/lib/providers/sagemaker/streaming.js +64 -6
  36. package/dist/lib/types/common.d.ts +3 -0
  37. package/dist/lib/types/openaiCompatible.d.ts +8 -7
  38. package/dist/lib/types/providers.d.ts +6 -0
  39. package/dist/lib/utils/pricing.js +15 -3
  40. package/dist/lib/utils/tokenUtils.js +38 -3
  41. package/dist/neurolink.js +18 -1
  42. package/dist/processors/media/index.d.ts +1 -1
  43. package/dist/processors/media/index.js +1 -1
  44. package/dist/providers/amazonBedrock.js +68 -6
  45. package/dist/providers/anthropic.js +50 -16
  46. package/dist/providers/googleAiStudio.js +29 -4
  47. package/dist/providers/googleNativeGemini3.js +10 -0
  48. package/dist/providers/googleVertex.js +150 -25
  49. package/dist/providers/litellm.js +13 -2
  50. package/dist/providers/openAI.js +13 -2
  51. package/dist/providers/openaiChatCompletionsBase.js +45 -10
  52. package/dist/providers/openaiChatCompletionsClient.d.ts +2 -14
  53. package/dist/providers/openaiChatCompletionsClient.js +20 -1
  54. package/dist/providers/sagemaker/language-model.js +5 -3
  55. package/dist/providers/sagemaker/parsers.js +18 -9
  56. package/dist/providers/sagemaker/streaming.d.ts +9 -0
  57. package/dist/providers/sagemaker/streaming.js +64 -6
  58. package/dist/types/common.d.ts +3 -0
  59. package/dist/types/openaiCompatible.d.ts +8 -7
  60. package/dist/types/providers.d.ts +6 -0
  61. package/dist/utils/pricing.js +15 -3
  62. package/dist/utils/tokenUtils.js +38 -3
  63. package/package.json +2 -1
@@ -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
- const inputCost = (tokens.input / 1000) * costInfo.input;
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
- if (result.usage) {
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
- if (result.usage) {
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
- if (result.usage) {
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
- // Note: The AI SDK bundles thinking tokens into promptTokens for Google models.
846
- // Separate reasoningTokens tracking will work when/if the AI SDK adds support.
847
- const usage = extractTokenUsage(generateResult.usage);
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
- const totalTokens = (usage?.inputTokens || 0) + (usage?.outputTokens || 0);
79
- const actualCost = await this.calculateActualCost(usage || { inputTokens: 0, outputTokens: 0 });
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
- const inputCost = (promptTokens / 1000) * costInfo.input;
130
- const outputCost = (completionTokens / 1000) * costInfo.output;
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): number;
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
- const inputCost = (tokens.input / 1000) * costInfo.input;
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
- if (result.usage) {
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
- if (result.usage) {
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
- if (result.usage) {
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
- // Note: The AI SDK bundles thinking tokens into promptTokens for Google models.
846
- // Separate reasoningTokens tracking will work when/if the AI SDK adds support.
847
- const usage = extractTokenUsage(generateResult.usage);
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
- const totalTokens = (usage?.inputTokens || 0) + (usage?.outputTokens || 0);
79
- const actualCost = await this.calculateActualCost(usage || { inputTokens: 0, outputTokens: 0 });
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
- const inputCost = (promptTokens / 1000) * costInfo.input;
130
- const outputCost = (completionTokens / 1000) * costInfo.output;
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) {
@@ -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,
@@ -12,8 +12,8 @@
12
12
  * videoProcessor,
13
13
  * isVideoFile,
14
14
  * processVideo,
15
- * type ProcessedVideo,
16
15
  * } from "./media/index.js";
16
+ * import type { ProcessedVideo } from "../../types/index.js";
17
17
  *
18
18
  * if (isVideoFile(file.mimetype, file.name)) {
19
19
  * const result = await processVideo(fileInfo);
@@ -12,8 +12,8 @@
12
12
  * videoProcessor,
13
13
  * isVideoFile,
14
14
  * processVideo,
15
- * type ProcessedVideo,
16
15
  * } from "./media/index.js";
16
+ * import type { ProcessedVideo } from "../../types/index.js";
17
17
  *
18
18
  * if (isVideoFile(file.mimetype, file.name)) {
19
19
  * const result = await processVideo(fileInfo);