@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
@@ -1469,11 +1469,14 @@ export class GoogleVertexProvider extends BaseProvider {
1469
1469
  // into a provider "prompt too long" rejection mid-loop.
1470
1470
  const contextGuard = createContextGuard(getContextWindowSize("vertex", modelName));
1471
1471
  let hitContextLimit = false;
1472
- // Track token usage across all steps
1473
- // promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
1472
+ // Track token usage across all steps. Every loop step is a separate
1473
+ // billed API call, so per-step usage is folded into these turn totals
1474
+ // after each step's chunk drain — assigning them directly from chunk
1475
+ // metadata would record only the last step of a multi-step tool loop.
1474
1476
  let totalInputTokens = 0;
1475
1477
  let totalOutputTokens = 0;
1476
1478
  let totalCacheReadTokens = 0;
1479
+ let totalReasoningTokens = 0;
1477
1480
  // Track text parts as they arrive from the SDK so the returned async
1478
1481
  // iterable yields multiple chunks instead of a single buffered chunk.
1479
1482
  // The CLI's chunk-count smoke test asserts > 1 stream chunks for any
@@ -1557,6 +1560,18 @@ export class GoogleVertexProvider extends BaseProvider {
1557
1560
  // lastFinishReason) — drives the single MALFORMED_FUNCTION_CALL
1558
1561
  // retry below.
1559
1562
  let stepFinishReason;
1563
+ // Per-step usage trackers for WRITE-THROUGH accumulation: within a
1564
+ // step's chunk stream the counts are latest-wins (promptTokenCount
1565
+ // arrives once in the final chunk; candidates/thoughts counts are
1566
+ // cumulative across chunks), so each chunk folds only the DELTA
1567
+ // over this step's previous value into the turn totals. The totals
1568
+ // are therefore correct at every point mid-drain — a step killed
1569
+ // mid-stream (abort / turn deadline / stall watchdog) still counts
1570
+ // the billed tokens it already reported.
1571
+ let stepInputTokens = 0;
1572
+ let stepOutputTokens = 0;
1573
+ let stepCacheReadTokens = 0;
1574
+ let stepReasoningTokens = 0;
1560
1575
  for await (const chunk of stream) {
1561
1576
  turnClock.noteProgress();
1562
1577
  // Extract raw parts from candidates FIRST
@@ -1591,18 +1606,34 @@ export class GoogleVertexProvider extends BaseProvider {
1591
1606
  // Take the latest promptTokenCount (usually only in final chunk)
1592
1607
  if (usageMetadata.promptTokenCount !== undefined &&
1593
1608
  usageMetadata.promptTokenCount > 0) {
1594
- totalInputTokens = usageMetadata.promptTokenCount;
1609
+ totalInputTokens +=
1610
+ usageMetadata.promptTokenCount - stepInputTokens;
1611
+ stepInputTokens = usageMetadata.promptTokenCount;
1595
1612
  // Feed the context guard the REAL prompt size of this call.
1596
1613
  contextGuard.noteUsage(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
1597
1614
  // cachedContentTokenCount is OVERLAPPING (a subset already inside
1598
- // promptTokenCount). Clamp to the prompt count so a later uncached
1599
- // step resets it to 0 instead of leaving a stale cached value.
1600
- totalCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
1615
+ // promptTokenCount). Clamp to the prompt count so an uncached
1616
+ // step reports 0 instead of a stale cached value.
1617
+ const chunkCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
1618
+ totalCacheReadTokens +=
1619
+ chunkCacheReadTokens - stepCacheReadTokens;
1620
+ stepCacheReadTokens = chunkCacheReadTokens;
1601
1621
  }
1602
1622
  // Take the latest candidatesTokenCount (accumulates through chunks)
1603
1623
  if (usageMetadata.candidatesTokenCount !== undefined &&
1604
1624
  usageMetadata.candidatesTokenCount > 0) {
1605
- totalOutputTokens = usageMetadata.candidatesTokenCount;
1625
+ totalOutputTokens +=
1626
+ usageMetadata.candidatesTokenCount - stepOutputTokens;
1627
+ stepOutputTokens = usageMetadata.candidatesTokenCount;
1628
+ }
1629
+ // thoughtsTokenCount (thinking tokens, billed at the output
1630
+ // rate) is NOT part of candidatesTokenCount — Gemini reports
1631
+ // totalTokenCount = prompt + candidates + thoughts.
1632
+ if (usageMetadata.thoughtsTokenCount !== undefined &&
1633
+ usageMetadata.thoughtsTokenCount > 0) {
1634
+ totalReasoningTokens +=
1635
+ usageMetadata.thoughtsTokenCount - stepReasoningTokens;
1636
+ stepReasoningTokens = usageMetadata.thoughtsTokenCount;
1606
1637
  }
1607
1638
  }
1608
1639
  }
@@ -1991,6 +2022,7 @@ export class GoogleVertexProvider extends BaseProvider {
1991
2022
  incrementalTextChunks.push(synth.text);
1992
2023
  totalInputTokens += synth.inputTokens;
1993
2024
  totalOutputTokens += synth.outputTokens;
2025
+ totalReasoningTokens += synth.reasoningTokens ?? 0;
1994
2026
  if (synth.finishReason) {
1995
2027
  lastFinishReason = synth.finishReason;
1996
2028
  }
@@ -2069,11 +2101,22 @@ export class GoogleVertexProvider extends BaseProvider {
2069
2101
  rawFinishReason: lastFinishReason,
2070
2102
  usage: {
2071
2103
  input: adjustedInputTokens,
2072
- output: totalOutputTokens,
2073
- total: adjustedInputTokens + totalCacheReadTokens + totalOutputTokens,
2104
+ // Thinking tokens are billed at the output rate but Gemini does NOT
2105
+ // include them in candidatesTokenCount (totalTokenCount = prompt +
2106
+ // candidates + thoughts), so they are folded into `output` — that is
2107
+ // what calculateCost bills at the output rate — with `reasoning`
2108
+ // reporting the thinking subset.
2109
+ output: totalOutputTokens + totalReasoningTokens,
2110
+ total: adjustedInputTokens +
2111
+ totalCacheReadTokens +
2112
+ totalOutputTokens +
2113
+ totalReasoningTokens,
2074
2114
  ...(totalCacheReadTokens > 0 && {
2075
2115
  cacheReadTokens: totalCacheReadTokens,
2076
2116
  }),
2117
+ ...(totalReasoningTokens > 0 && {
2118
+ reasoning: totalReasoningTokens,
2119
+ }),
2077
2120
  },
2078
2121
  toolCalls: externalToolCalls.map((tc) => ({
2079
2122
  toolName: tc.toolName,
@@ -2409,11 +2452,14 @@ export class GoogleVertexProvider extends BaseProvider {
2409
2452
  // into a provider "prompt too long" rejection mid-loop.
2410
2453
  const contextGuard = createContextGuard(getContextWindowSize("vertex", modelName));
2411
2454
  let hitContextLimit = false;
2412
- // Track token usage across all steps
2413
- // promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
2455
+ // Track token usage across all steps. Every loop step is a separate
2456
+ // billed API call, so per-step usage is folded into these turn totals
2457
+ // after each step's chunk drain — assigning them directly from chunk
2458
+ // metadata would record only the last step of a multi-step tool loop.
2414
2459
  let totalInputTokens = 0;
2415
2460
  let totalOutputTokens = 0;
2416
2461
  let totalCacheReadTokens = 0;
2462
+ let totalReasoningTokens = 0;
2417
2463
  // Abort scaffolding (mirrors executeNativeAnthropicStream). The native
2418
2464
  // Gemini SDK cancels via config.abortSignal, so drive an internal
2419
2465
  // AbortController: the caller's signal and the turn clock's watchdogs
@@ -2488,6 +2534,18 @@ export class GoogleVertexProvider extends BaseProvider {
2488
2534
  // lastFinishReason) — drives the single MALFORMED_FUNCTION_CALL
2489
2535
  // retry below.
2490
2536
  let stepFinishReason;
2537
+ // Per-step usage trackers for WRITE-THROUGH accumulation: within a
2538
+ // step's chunk stream the counts are latest-wins (promptTokenCount
2539
+ // arrives once in the final chunk; candidates/thoughts counts are
2540
+ // cumulative across chunks), so each chunk folds only the DELTA
2541
+ // over this step's previous value into the turn totals. The totals
2542
+ // are therefore correct at every point mid-drain — a step killed
2543
+ // mid-stream (abort / turn deadline / stall watchdog) still counts
2544
+ // the billed tokens it already reported.
2545
+ let stepInputTokens = 0;
2546
+ let stepOutputTokens = 0;
2547
+ let stepCacheReadTokens = 0;
2548
+ let stepReasoningTokens = 0;
2491
2549
  // Collect all chunks from stream
2492
2550
  for await (const chunk of stream) {
2493
2551
  turnClock.noteProgress();
@@ -2518,18 +2576,34 @@ export class GoogleVertexProvider extends BaseProvider {
2518
2576
  // Take the latest promptTokenCount (usually only in final chunk)
2519
2577
  if (usageMetadata.promptTokenCount !== undefined &&
2520
2578
  usageMetadata.promptTokenCount > 0) {
2521
- totalInputTokens = usageMetadata.promptTokenCount;
2579
+ totalInputTokens +=
2580
+ usageMetadata.promptTokenCount - stepInputTokens;
2581
+ stepInputTokens = usageMetadata.promptTokenCount;
2522
2582
  // Feed the context guard the REAL prompt size of this call.
2523
2583
  contextGuard.noteUsage(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
2524
2584
  // cachedContentTokenCount is OVERLAPPING (a subset already inside
2525
- // promptTokenCount). Clamp to the prompt count so a later uncached
2526
- // step resets it to 0 instead of leaving a stale cached value.
2527
- totalCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
2585
+ // promptTokenCount). Clamp to the prompt count so an uncached
2586
+ // step reports 0 instead of a stale cached value.
2587
+ const chunkCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
2588
+ totalCacheReadTokens +=
2589
+ chunkCacheReadTokens - stepCacheReadTokens;
2590
+ stepCacheReadTokens = chunkCacheReadTokens;
2528
2591
  }
2529
2592
  // Take the latest candidatesTokenCount (accumulates through chunks)
2530
2593
  if (usageMetadata.candidatesTokenCount !== undefined &&
2531
2594
  usageMetadata.candidatesTokenCount > 0) {
2532
- totalOutputTokens = usageMetadata.candidatesTokenCount;
2595
+ totalOutputTokens +=
2596
+ usageMetadata.candidatesTokenCount - stepOutputTokens;
2597
+ stepOutputTokens = usageMetadata.candidatesTokenCount;
2598
+ }
2599
+ // thoughtsTokenCount (thinking tokens, billed at the output
2600
+ // rate) is NOT part of candidatesTokenCount — Gemini reports
2601
+ // totalTokenCount = prompt + candidates + thoughts.
2602
+ if (usageMetadata.thoughtsTokenCount !== undefined &&
2603
+ usageMetadata.thoughtsTokenCount > 0) {
2604
+ totalReasoningTokens +=
2605
+ usageMetadata.thoughtsTokenCount - stepReasoningTokens;
2606
+ stepReasoningTokens = usageMetadata.thoughtsTokenCount;
2533
2607
  }
2534
2608
  }
2535
2609
  }
@@ -2912,6 +2986,7 @@ export class GoogleVertexProvider extends BaseProvider {
2912
2986
  finalText = synth.text;
2913
2987
  totalInputTokens += synth.inputTokens;
2914
2988
  totalOutputTokens += synth.outputTokens;
2989
+ totalReasoningTokens += synth.reasoningTokens ?? 0;
2915
2990
  if (synth.finishReason) {
2916
2991
  lastFinishReason = synth.finishReason;
2917
2992
  }
@@ -2974,12 +3049,26 @@ export class GoogleVertexProvider extends BaseProvider {
2974
3049
  stepsUsed: step,
2975
3050
  usage: {
2976
3051
  input: adjustedInputTokens,
2977
- output: totalOutputTokens,
2978
- total: adjustedInputTokens + totalCacheReadTokens + totalOutputTokens,
3052
+ // Thinking tokens are billed at the output rate but Gemini does NOT
3053
+ // include them in candidatesTokenCount (totalTokenCount = prompt +
3054
+ // candidates + thoughts), so they are folded into `output` — that is
3055
+ // what calculateCost bills at the output rate — with `reasoning`
3056
+ // reporting the thinking subset.
3057
+ output: totalOutputTokens + totalReasoningTokens,
3058
+ total: adjustedInputTokens +
3059
+ totalCacheReadTokens +
3060
+ totalOutputTokens +
3061
+ totalReasoningTokens,
2979
3062
  ...(totalCacheReadTokens > 0 && {
2980
3063
  cacheReadTokens: totalCacheReadTokens,
2981
3064
  }),
3065
+ ...(totalReasoningTokens > 0 && {
3066
+ reasoning: totalReasoningTokens,
3067
+ }),
2982
3068
  },
3069
+ ...(totalReasoningTokens > 0 && {
3070
+ reasoningTokens: totalReasoningTokens,
3071
+ }),
2983
3072
  responseTime,
2984
3073
  toolsUsed: externalToolCalls.map((tc) => tc.toolName),
2985
3074
  toolExecutions: resolveToolExecutionRecords(options, externalToolExecutions),
@@ -3016,7 +3105,7 @@ export class GoogleVertexProvider extends BaseProvider {
3016
3105
  // Already aborted — never issue the synth request (would add +300s after a
3017
3106
  // blown budget). Return empty so the caller maps to the graceful message.
3018
3107
  if (abortSignal?.aborted) {
3019
- return { text: "", inputTokens: 0, outputTokens: 0 };
3108
+ return { text: "", inputTokens: 0, outputTokens: 0, reasoningTokens: 0 };
3020
3109
  }
3021
3110
  try {
3022
3111
  // Shallow clone so the loop's config is never mutated; dropping the
@@ -3053,6 +3142,7 @@ export class GoogleVertexProvider extends BaseProvider {
3053
3142
  let finishReason;
3054
3143
  let inputTokens = 0;
3055
3144
  let outputTokens = 0;
3145
+ let reasoningTokens = 0;
3056
3146
  for await (const chunk of stream) {
3057
3147
  const chunkRecord = chunk;
3058
3148
  const candidates = chunkRecord.candidates;
@@ -3075,18 +3165,28 @@ export class GoogleVertexProvider extends BaseProvider {
3075
3165
  usageMetadata.candidatesTokenCount > 0) {
3076
3166
  outputTokens = usageMetadata.candidatesTokenCount;
3077
3167
  }
3168
+ if (usageMetadata.thoughtsTokenCount !== undefined &&
3169
+ usageMetadata.thoughtsTokenCount > 0) {
3170
+ reasoningTokens = usageMetadata.thoughtsTokenCount;
3171
+ }
3078
3172
  }
3079
3173
  }
3080
3174
  const text = parts
3081
3175
  .filter((part) => typeof part.text === "string")
3082
3176
  .map((part) => part.text)
3083
3177
  .join("");
3084
- return { text, finishReason, inputTokens, outputTokens };
3178
+ return {
3179
+ text,
3180
+ finishReason,
3181
+ inputTokens,
3182
+ outputTokens,
3183
+ reasoningTokens,
3184
+ };
3085
3185
  })(), timeoutMs, "Gemini synthesis call timed out");
3086
3186
  }
3087
3187
  catch (error) {
3088
3188
  logger.warn("[GoogleVertex] Tools-disabled synthesis call failed; falling back to placeholder", { error: error instanceof Error ? error.message : String(error) });
3089
- return { text: "", inputTokens: 0, outputTokens: 0 };
3189
+ return { text: "", inputTokens: 0, outputTokens: 0, reasoningTokens: 0 };
3090
3190
  }
3091
3191
  }
3092
3192
  /**
@@ -3651,7 +3751,14 @@ export class GoogleVertexProvider extends BaseProvider {
3651
3751
  turnCacheUsage.creation1h += stepCacheCreation1h;
3652
3752
  usage.input += response.usage?.input_tokens || 0;
3653
3753
  usage.output += response.usage?.output_tokens || 0;
3654
- usage.total = usage.input + usage.output;
3754
+ // Anthropic's input_tokens is only the UNCACHED remainder; cache
3755
+ // reads/writes are billed tokens reported separately, so the
3756
+ // total must include them (matches proxyTracer + anthropic.ts).
3757
+ usage.total =
3758
+ usage.input +
3759
+ usage.output +
3760
+ turnCacheUsage.read +
3761
+ turnCacheUsage.creation;
3655
3762
  lastStopReason = response.stop_reason;
3656
3763
  // Feed the context guard the FULL prompt size of this call
3657
3764
  // (uncached input + cache reads/writes).
@@ -4102,7 +4209,6 @@ export class GoogleVertexProvider extends BaseProvider {
4102
4209
  activeStream = undefined;
4103
4210
  usage.input += response.usage?.input_tokens || 0;
4104
4211
  usage.output += response.usage?.output_tokens || 0;
4105
- usage.total = usage.input + usage.output;
4106
4212
  turnCacheUsage.read +=
4107
4213
  response.usage?.cache_read_input_tokens ?? 0;
4108
4214
  turnCacheUsage.creation +=
@@ -4111,6 +4217,13 @@ export class GoogleVertexProvider extends BaseProvider {
4111
4217
  response.usage?.cache_creation?.ephemeral_5m_input_tokens ?? 0;
4112
4218
  turnCacheUsage.creation1h +=
4113
4219
  response.usage?.cache_creation?.ephemeral_1h_input_tokens ?? 0;
4220
+ // Total counts cache reads/writes — input_tokens is only the
4221
+ // uncached remainder.
4222
+ usage.total =
4223
+ usage.input +
4224
+ usage.output +
4225
+ turnCacheUsage.read +
4226
+ turnCacheUsage.creation;
4114
4227
  lastStopReason = response.stop_reason;
4115
4228
  const forcedFinalResult = response.content.find((block) => block.type === "tool_use" && block.name === "final_result");
4116
4229
  if (forcedFinalResult) {
@@ -4196,7 +4309,6 @@ export class GoogleVertexProvider extends BaseProvider {
4196
4309
  activeStream = undefined;
4197
4310
  usage.input += response.usage?.input_tokens || 0;
4198
4311
  usage.output += response.usage?.output_tokens || 0;
4199
- usage.total = usage.input + usage.output;
4200
4312
  turnCacheUsage.read +=
4201
4313
  response.usage?.cache_read_input_tokens ?? 0;
4202
4314
  turnCacheUsage.creation +=
@@ -4207,6 +4319,13 @@ export class GoogleVertexProvider extends BaseProvider {
4207
4319
  turnCacheUsage.creation1h +=
4208
4320
  response.usage?.cache_creation?.ephemeral_1h_input_tokens ??
4209
4321
  0;
4322
+ // Total counts cache reads/writes — input_tokens is only the
4323
+ // uncached remainder.
4324
+ usage.total =
4325
+ usage.input +
4326
+ usage.output +
4327
+ turnCacheUsage.read +
4328
+ turnCacheUsage.creation;
4210
4329
  lastStopReason = response.stop_reason;
4211
4330
  const backstopText = response.content
4212
4331
  .filter((block) => block.type === "text")
@@ -5408,7 +5527,13 @@ export class GoogleVertexProvider extends BaseProvider {
5408
5527
  usage: {
5409
5528
  input: totalInputTokens,
5410
5529
  output: totalOutputTokens,
5411
- total: totalInputTokens + totalOutputTokens,
5530
+ // Anthropic's input_tokens is only the UNCACHED remainder; cache
5531
+ // reads/writes are billed tokens reported separately, so the total
5532
+ // must include them (matches proxyTracer + anthropic.ts).
5533
+ total: totalInputTokens +
5534
+ totalOutputTokens +
5535
+ totalCacheReadTokens +
5536
+ totalCacheCreationTokens,
5412
5537
  ...(totalCacheReadTokens > 0 && {
5413
5538
  cacheReadTokens: totalCacheReadTokens,
5414
5539
  }),
@@ -283,12 +283,23 @@ export class LiteLLMProvider extends OpenAIChatCompletionsProvider {
283
283
  };
284
284
  return {
285
285
  onUsage: (usage) => {
286
- span.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens);
286
+ // promptTokens is the uncached remainder — the attribute reports the
287
+ // full prompt (uncached + cache read/creation), and the cache fields
288
+ // let calculateCost price tiers.
289
+ span.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens +
290
+ (usage.cacheReadTokens ?? 0) +
291
+ (usage.cacheCreationTokens ?? 0));
287
292
  span.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens);
288
- const cost = calculateCost(this.providerName, this.modelName, {
293
+ const cost = calculateCost(this.providerName, modelId, {
289
294
  input: usage.promptTokens,
290
295
  output: usage.completionTokens,
291
296
  total: usage.totalTokens,
297
+ ...(usage.cacheReadTokens
298
+ ? { cacheReadTokens: usage.cacheReadTokens }
299
+ : {}),
300
+ ...(usage.cacheCreationTokens
301
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
302
+ : {}),
292
303
  });
293
304
  if (cost && cost > 0) {
294
305
  span.setAttribute("neurolink.cost", cost);
@@ -157,12 +157,23 @@ export class OpenAIProvider extends OpenAIChatCompletionsProvider {
157
157
  };
158
158
  return {
159
159
  onUsage: (usage) => {
160
- span.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens);
160
+ // promptTokens is the uncached remainder — the attribute reports the
161
+ // full prompt (uncached + cache read/creation), and the cache fields
162
+ // let calculateCost price tiers.
163
+ span.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens +
164
+ (usage.cacheReadTokens ?? 0) +
165
+ (usage.cacheCreationTokens ?? 0));
161
166
  span.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens);
162
- const cost = calculateCost(this.providerName, this.modelName, {
167
+ const cost = calculateCost(this.providerName, modelId, {
163
168
  input: usage.promptTokens,
164
169
  output: usage.completionTokens,
165
170
  total: usage.totalTokens,
171
+ ...(usage.cacheReadTokens
172
+ ? { cacheReadTokens: usage.cacheReadTokens }
173
+ : {}),
174
+ ...(usage.cacheCreationTokens
175
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
176
+ : {}),
166
177
  });
167
178
  if (cost && cost > 0) {
168
179
  span.setAttribute("neurolink.cost", cost);
@@ -505,8 +505,21 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
505
505
  usage: {
506
506
  inputTokens: {
507
507
  total: json.usage?.prompt_tokens,
508
- noCache: json.usage?.prompt_tokens,
509
- cacheRead: undefined,
508
+ // cached_tokens is OVERLAPPING (a subset of prompt_tokens), so
509
+ // noCache is the remainder and cacheRead is clamped to the
510
+ // prompt total — some gateways report inconsistent usage where
511
+ // cached_tokens exceeds prompt_tokens (mirrors the
512
+ // reasoning_tokens clamp on the output side below).
513
+ noCache: json.usage?.prompt_tokens !== undefined &&
514
+ json.usage?.prompt_tokens_details?.cached_tokens !== undefined
515
+ ? Math.max(0, json.usage.prompt_tokens -
516
+ json.usage.prompt_tokens_details.cached_tokens)
517
+ : json.usage?.prompt_tokens,
518
+ cacheRead: json.usage?.prompt_tokens !== undefined &&
519
+ json.usage?.prompt_tokens_details?.cached_tokens !== undefined
520
+ ? Math.min(json.usage.prompt_tokens_details.cached_tokens, json.usage.prompt_tokens)
521
+ : json.usage?.prompt_tokens_details?.cached_tokens,
522
+ // OpenAI-style APIs do not report cache writes.
510
523
  cacheWrite: undefined,
511
524
  },
512
525
  outputTokens: {
@@ -727,9 +740,33 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
727
740
  }
728
741
  async runStreamLoop(args) {
729
742
  const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, resolveUsage, resolveFinish, } = args;
743
+ // Hoisted above the try so the catch can resolve the usage accumulated
744
+ // by steps that completed BEFORE the failure — those steps were billed.
745
+ let stepFinish = null;
746
+ let stepUsage;
747
+ // Aggregated DeferredUsage from whatever accumulated in stepUsage.
748
+ // prompt_tokens is OVERLAPPING with cached_tokens (a subset), so the
749
+ // uncached remainder goes in promptTokens and the cached part in
750
+ // cacheReadTokens — the non-overlapping convention extractTokenUsage
751
+ // and calculateCost expect. reasoning_tokens stays a subset of
752
+ // completionTokens (informational).
753
+ const toDeferredUsage = () => {
754
+ const promptTokens = stepUsage?.prompt_tokens ?? 0;
755
+ const completionTokens = stepUsage?.completion_tokens ?? 0;
756
+ const cachedTokens = Math.min(stepUsage?.prompt_tokens_details?.cached_tokens ?? 0, promptTokens);
757
+ // Clamped like cached_tokens above: reasoning is a SUBSET of
758
+ // completion_tokens, but some gateways report inconsistent values.
759
+ const reasoningTokens = Math.min(Math.max(0, stepUsage?.completion_tokens_details?.reasoning_tokens ?? 0), completionTokens);
760
+ return {
761
+ promptTokens: promptTokens - cachedTokens,
762
+ completionTokens,
763
+ // Gateways that omit total_tokens must not collapse to 0.
764
+ totalTokens: stepUsage?.total_tokens || promptTokens + completionTokens,
765
+ ...(cachedTokens > 0 ? { cacheReadTokens: cachedTokens } : {}),
766
+ ...(reasoningTokens > 0 ? { reasoningTokens } : {}),
767
+ };
768
+ };
730
769
  try {
731
- let stepFinish = null;
732
- let stepUsage;
733
770
  // May grow mid-turn: hydrated tools with wire-unsafe names need
734
771
  // reverse-mapping even when the initial name set required none.
735
772
  let effectiveToolNameFromWire = toolNameFromWire;
@@ -787,11 +824,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
787
824
  options,
788
825
  });
789
826
  }
790
- resolveUsage({
791
- promptTokens: stepUsage?.prompt_tokens ?? 0,
792
- completionTokens: stepUsage?.completion_tokens ?? 0,
793
- totalTokens: stepUsage?.total_tokens ?? 0,
794
- });
827
+ resolveUsage(toDeferredUsage());
795
828
  resolveFinish(stepFinish ?? "stop");
796
829
  pushChunk({ done: true });
797
830
  return {
@@ -803,7 +836,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
803
836
  logger.error(`${this.providerName}: Stream error`, {
804
837
  error: err instanceof Error ? err.message : String(err),
805
838
  });
806
- resolveUsage({ promptTokens: 0, completionTokens: 0, totalTokens: 0 });
839
+ // Steps that completed before the failure were billed — report them
840
+ // instead of zeroing the whole turn.
841
+ resolveUsage(toDeferredUsage());
807
842
  resolveFinish("error");
808
843
  pushChunk({ done: true });
809
844
  throw err;
@@ -13,7 +13,7 @@
13
13
  * Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
14
14
  * module is to be the native replacement for the AI SDK's OpenAI wrapper.
15
15
  */
16
- import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
16
+ import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatUsage, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
17
17
  export declare const stripTrailingSlash: (s: string) => string;
18
18
  /**
19
19
  * Build a bijective original ↔ wire tool-name map. Returns undefined when
@@ -71,16 +71,4 @@ export declare const createChunkQueue: () => {
71
71
  pushChunk: (c: OpenAICompatStreamChunk) => void;
72
72
  nextChunk: () => Promise<OpenAICompatStreamChunk>;
73
73
  };
74
- export declare const mergeUsage: (a: {
75
- prompt_tokens?: number;
76
- completion_tokens?: number;
77
- total_tokens?: number;
78
- } | undefined, b: {
79
- prompt_tokens?: number;
80
- completion_tokens?: number;
81
- total_tokens?: number;
82
- } | undefined) => {
83
- prompt_tokens?: number;
84
- completion_tokens?: number;
85
- total_tokens?: number;
86
- } | undefined;
74
+ export declare const mergeUsage: (a: OpenAICompatUsage | undefined, b: OpenAICompatUsage | undefined) => OpenAICompatUsage | undefined;
@@ -673,10 +673,29 @@ export const mergeUsage = (a, b) => {
673
673
  if (!b) {
674
674
  return a;
675
675
  }
676
+ // Sum the nested details too — narrowing to the three flat scalars here
677
+ // silently dropped cached_tokens / reasoning_tokens the first time two
678
+ // steps were merged (single-step streams passed the object through
679
+ // untouched, so the loss only showed up in multi-step tool loops).
680
+ const cachedTokens = (a.prompt_tokens_details?.cached_tokens ?? 0) +
681
+ (b.prompt_tokens_details?.cached_tokens ?? 0);
682
+ const reasoningTokens = (a.completion_tokens_details?.reasoning_tokens ?? 0) +
683
+ (b.completion_tokens_details?.reasoning_tokens ?? 0);
684
+ // A step that omits total_tokens contributes its prompt+completion sum
685
+ // instead of 0 — otherwise one totals-reporting step next to one that
686
+ // omits it yields a nonzero-but-partial aggregate that downstream
687
+ // consumers trust over their own prompt+completion fallback.
688
+ const stepTotal = (u) => u.total_tokens || (u.prompt_tokens ?? 0) + (u.completion_tokens ?? 0);
676
689
  return {
677
690
  prompt_tokens: (a.prompt_tokens ?? 0) + (b.prompt_tokens ?? 0),
678
691
  completion_tokens: (a.completion_tokens ?? 0) + (b.completion_tokens ?? 0),
679
- total_tokens: (a.total_tokens ?? 0) + (b.total_tokens ?? 0),
692
+ total_tokens: stepTotal(a) + stepTotal(b),
693
+ ...(cachedTokens > 0
694
+ ? { prompt_tokens_details: { cached_tokens: cachedTokens } }
695
+ : {}),
696
+ ...(reasoningTokens > 0
697
+ ? { completion_tokens_details: { reasoning_tokens: reasoningTokens } }
698
+ : {}),
680
699
  };
681
700
  };
682
701
  //# sourceMappingURL=openaiChatCompletionsClient.js.map
@@ -7,7 +7,7 @@
7
7
  import { randomUUID } from "crypto";
8
8
  import { SageMakerRuntimeClient } from "./client.js";
9
9
  import { handleSageMakerError } from "./errors.js";
10
- import { estimateTokenUsage, createSageMakerStream } from "./streaming.js";
10
+ import { estimateTokenUsage, createSageMakerStream, parseUsageFromResponseBody, } from "./streaming.js";
11
11
  import { createAdaptiveSemaphore } from "./adaptive-semaphore.js";
12
12
  import { logger } from "../../utils/logger.js";
13
13
  /**
@@ -140,8 +140,10 @@ export class SageMakerLanguageModel {
140
140
  const generatedText = this.extractTextFromResponse(responseBody);
141
141
  // Extract tool calls if present (Phase 4 enhancement)
142
142
  const toolCalls = this.extractToolCallsFromResponse(responseBody);
143
- // Calculate token usage
144
- const usage = estimateTokenUsage(promptText, generatedText);
143
+ // Prefer the endpoint's REAL token counts; only fall back to the
144
+ // char-heuristic estimate when the response reports none.
145
+ const usage = parseUsageFromResponseBody(responseBody) ??
146
+ estimateTokenUsage(promptText, generatedText);
145
147
  // Determine finish reason based on response content
146
148
  let finishReason = "stop";
147
149
  if (toolCalls && toolCalls.length > 0) {
@@ -276,10 +276,13 @@ export class HuggingFaceStreamParser extends BaseStreamingParser {
276
276
  return undefined;
277
277
  }
278
278
  const tokens = details.tokens;
279
+ const promptTokens = Number(tokens.input) || 0;
280
+ const completionTokens = Number(tokens.generated) || 0;
279
281
  return {
280
- promptTokens: Number(tokens.input) || 0,
281
- completionTokens: Number(tokens.generated) || 0,
282
- total: Number(tokens.total) || 0,
282
+ promptTokens,
283
+ completionTokens,
284
+ // Endpoints that omit the total must not report 0 next to real counts.
285
+ total: Number(tokens.total) || promptTokens + completionTokens,
283
286
  };
284
287
  }
285
288
  mapFinishReason(reason) {
@@ -447,10 +450,13 @@ export class LlamaStreamParser extends BaseStreamingParser {
447
450
  return toolCall;
448
451
  }
449
452
  parseLlamaUsage(usage) {
453
+ const promptTokens = Number(usage.prompt_tokens) || 0;
454
+ const completionTokens = Number(usage.completion_tokens) || 0;
450
455
  return {
451
- promptTokens: Number(usage.prompt_tokens) || 0,
452
- completionTokens: Number(usage.completion_tokens) || 0,
453
- total: Number(usage.total_tokens) || 0,
456
+ promptTokens,
457
+ completionTokens,
458
+ // Endpoints that omit total_tokens must not report 0 next to real counts.
459
+ total: Number(usage.total_tokens) || promptTokens + completionTokens,
454
460
  };
455
461
  }
456
462
  mapFinishReason(reason) {
@@ -569,10 +575,13 @@ export class CustomStreamParser extends BaseStreamingParser {
569
575
  }
570
576
  parseCustomUsage(data) {
571
577
  const usage = (data.usage || data.tokens || {});
578
+ const promptTokens = Number(usage.prompt_tokens || usage.input_tokens) || 0;
579
+ const completionTokens = Number(usage.completion_tokens || usage.output_tokens) || 0;
572
580
  return {
573
- promptTokens: Number(usage.prompt_tokens || usage.input_tokens) || 0,
574
- completionTokens: Number(usage.completion_tokens || usage.output_tokens) || 0,
575
- total: Number(usage.total_tokens) || 0,
581
+ promptTokens,
582
+ completionTokens,
583
+ // Endpoints that omit total_tokens must not report 0 next to real counts.
584
+ total: Number(usage.total_tokens) || promptTokens + completionTokens,
576
585
  };
577
586
  }
578
587
  }
@@ -29,6 +29,15 @@ export declare function createSyntheticStream(text: string, usage: SageMakerUsag
29
29
  onChunk?: (chunk: SageMakerStreamChunk) => void;
30
30
  onComplete?: (usage: SageMakerUsage) => void;
31
31
  }): Promise<ReadableStream<unknown>>;
32
+ /**
33
+ * Extract REAL token usage from a complete (non-streaming) SageMaker
34
+ * response body when the endpoint reports it. Covers the OpenAI-compatible
35
+ * shape (vLLM / TGI-OpenAI: usage.prompt_tokens/completion_tokens), the
36
+ * generic input_tokens/output_tokens shape, and the HuggingFace TGI shape
37
+ * (details.tokens.input/generated). Returns undefined when no real counts
38
+ * are present so callers can fall back to estimateTokenUsage.
39
+ */
40
+ export declare function parseUsageFromResponseBody(body: Record<string, unknown> | Array<Record<string, unknown>>): SageMakerUsage | undefined;
32
41
  /**
33
42
  * Estimate token usage from text content
34
43
  *