@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
@@ -87,12 +87,12 @@ async function createProtocolSpecificStream(responseStream, parser, capability,
87
87
  if (done) {
88
88
  // Stream ended - send final chunk if needed
89
89
  if (!finalUsage && accumulatedText) {
90
- finalUsage = estimateTokenUsage("", accumulatedText);
90
+ finalUsage = estimateTokenUsage(options.prompt ?? "", accumulatedText);
91
91
  }
92
92
  const finalChunk = {
93
93
  type: "finish",
94
94
  finishReason: "stop",
95
- usage: finalUsage,
95
+ usage: toLanguageModelV2Usage(finalUsage),
96
96
  };
97
97
  controller.enqueue(finalChunk);
98
98
  options.onComplete?.(finalUsage || {
@@ -182,11 +182,11 @@ async function createProtocolSpecificStream(responseStream, parser, capability,
182
182
  if (parser.isComplete(chunk)) {
183
183
  finalUsage =
184
184
  parser.extractUsage(chunk) ||
185
- estimateTokenUsage("", accumulatedText);
185
+ estimateTokenUsage(options.prompt ?? "", accumulatedText);
186
186
  const finalChunk = {
187
187
  type: "finish",
188
188
  finishReason: chunk.finishReason || "stop",
189
- usage: finalUsage,
189
+ usage: toLanguageModelV2Usage(finalUsage),
190
190
  };
191
191
  controller.enqueue(finalChunk);
192
192
  options.onComplete?.(finalUsage);
@@ -271,7 +271,7 @@ async function createSyntheticStreamFromResponse(responseStream, options) {
271
271
  const finalChunk = {
272
272
  type: "finish",
273
273
  finishReason: "stop",
274
- usage,
274
+ usage: toLanguageModelV2Usage(usage),
275
275
  };
276
276
  controller.enqueue(finalChunk);
277
277
  options.onComplete?.(usage);
@@ -305,7 +305,7 @@ export async function createSyntheticStream(text, usage, options = {}) {
305
305
  const finalChunk = {
306
306
  type: "finish",
307
307
  finishReason: "stop",
308
- usage,
308
+ usage: toLanguageModelV2Usage(usage),
309
309
  };
310
310
  controller.enqueue(finalChunk);
311
311
  options.onComplete?.(usage);
@@ -313,6 +313,64 @@ export async function createSyntheticStream(text, usage, options = {}) {
313
313
  },
314
314
  });
315
315
  }
316
+ /**
317
+ * Map the internal SageMakerUsage shape to AI SDK v2 usage keys. Stream
318
+ * finish parts previously carried promptTokens/completionTokens, but the
319
+ * SDK's v2→v3 bridge reads inputTokens/outputTokens/totalTokens — every
320
+ * streamed SageMaker turn resolved to undefined (zero) usage.
321
+ */
322
+ function toLanguageModelV2Usage(usage) {
323
+ const inputTokens = usage?.promptTokens ?? 0;
324
+ const outputTokens = usage?.completionTokens ?? 0;
325
+ return {
326
+ inputTokens,
327
+ outputTokens,
328
+ totalTokens: usage?.total || inputTokens + outputTokens,
329
+ };
330
+ }
331
+ /**
332
+ * Extract REAL token usage from a complete (non-streaming) SageMaker
333
+ * response body when the endpoint reports it. Covers the OpenAI-compatible
334
+ * shape (vLLM / TGI-OpenAI: usage.prompt_tokens/completion_tokens), the
335
+ * generic input_tokens/output_tokens shape, and the HuggingFace TGI shape
336
+ * (details.tokens.input/generated). Returns undefined when no real counts
337
+ * are present so callers can fall back to estimateTokenUsage.
338
+ */
339
+ export function parseUsageFromResponseBody(body) {
340
+ // TGI-style endpoints wrap the response in an array
341
+ // ([{ generated_text, details: { tokens } }]) — normalize to the first
342
+ // item so those endpoints get real usage instead of the char estimate.
343
+ const responseBody = Array.isArray(body) ? body[0] : body;
344
+ if (!responseBody || typeof responseBody !== "object") {
345
+ return undefined;
346
+ }
347
+ const usageRecord = (responseBody.usage ?? responseBody.tokens);
348
+ if (usageRecord && typeof usageRecord === "object") {
349
+ const promptTokens = Number(usageRecord.prompt_tokens ?? usageRecord.input_tokens) || 0;
350
+ const completionTokens = Number(usageRecord.completion_tokens ?? usageRecord.output_tokens) || 0;
351
+ if (promptTokens > 0 || completionTokens > 0) {
352
+ return {
353
+ promptTokens,
354
+ completionTokens,
355
+ total: Number(usageRecord.total_tokens) || promptTokens + completionTokens,
356
+ };
357
+ }
358
+ }
359
+ const details = responseBody.details;
360
+ const tokens = details?.tokens;
361
+ if (tokens && typeof tokens === "object") {
362
+ const promptTokens = Number(tokens.input) || 0;
363
+ const completionTokens = Number(tokens.generated) || 0;
364
+ if (promptTokens > 0 || completionTokens > 0) {
365
+ return {
366
+ promptTokens,
367
+ completionTokens,
368
+ total: Number(tokens.total) || promptTokens + completionTokens,
369
+ };
370
+ }
371
+ }
372
+ return undefined;
373
+ }
316
374
  /**
317
375
  * Estimate token usage from text content
318
376
  *
@@ -225,6 +225,7 @@ export type RawUsageObject = {
225
225
  cacheReadTokens?: number;
226
226
  cachedInputTokens?: number;
227
227
  inputTokenDetails?: {
228
+ noCacheTokens?: number;
228
229
  cacheReadTokens?: number;
229
230
  cacheWriteTokens?: number;
230
231
  };
@@ -248,6 +249,8 @@ export type DeferredUsage = {
248
249
  totalTokens: number;
249
250
  cacheReadTokens?: number;
250
251
  cacheCreationTokens?: number;
252
+ /** Reasoning/thinking tokens — a SUBSET already included in completionTokens. */
253
+ reasoningTokens?: number;
251
254
  };
252
255
  /**
253
256
  * Options for token extraction from raw usage objects.
@@ -1,4 +1,4 @@
1
- import type { NeuroLinkEvents, TypedEventEmitter } from "./common.js";
1
+ import type { DeferredUsage, NeuroLinkEvents, TypedEventEmitter } from "./common.js";
2
2
  import type { JSONSchema7 } from "./middleware.js";
3
3
  import type { LanguageModelV3, LanguageModelV3CallOptions } from "./middleware.js";
4
4
  import type { StreamOptions } from "./stream.js";
@@ -271,12 +271,13 @@ export type OpenAICompatBuildBodyArgs = {
271
271
  * the deferred analytics promises.
272
272
  */
273
273
  export type OpenAICompatStreamLifecycleListeners = {
274
- /** Fired once the deferred usage promise resolves with the final aggregated token counts. */
275
- onUsage?: (usage: {
276
- promptTokens: number;
277
- completionTokens: number;
278
- totalTokens: number;
279
- }) => void;
274
+ /**
275
+ * Fired once the deferred usage promise resolves with the final aggregated
276
+ * token counts. promptTokens is the UNCACHED remainder; cacheReadTokens
277
+ * carries the cached portion (non-overlapping convention), and
278
+ * reasoningTokens is a subset of completionTokens.
279
+ */
280
+ onUsage?: (usage: DeferredUsage) => void;
280
281
  /**
281
282
  * Fired once the deferred finish promise resolves. `reason` is "stop",
282
283
  * "length", "tool-calls", "content-filter", or "error". When the loop
@@ -1623,6 +1623,12 @@ export type CollectedChunkResult = {
1623
1623
  cacheReadTokens?: number;
1624
1624
  /** Cache creation tokens (symmetry; Gemini does not emit this). */
1625
1625
  cacheCreationTokens?: number;
1626
+ /**
1627
+ * Gemini thinking tokens (usageMetadata.thoughtsTokenCount). Billed at the
1628
+ * output rate but NOT included in candidatesTokenCount — Gemini reports
1629
+ * totalTokenCount = prompt + candidates + thoughts.
1630
+ */
1631
+ reasoningTokens?: number;
1626
1632
  };
1627
1633
  /** Push-based text channel for incremental streaming. */
1628
1634
  export type TextChannel = {
@@ -653,15 +653,27 @@ function findRates(provider, model) {
653
653
  if (!providerPricing) {
654
654
  return undefined;
655
655
  }
656
+ // Bedrock model IDs carry region/vendor prefixes ("us.anthropic.claude-…",
657
+ // "global.anthropic.claude-…", "anthropic.claude-…") or arrive as full
658
+ // inference-profile ARNs ("arn:aws:bedrock:…:inference-profile/us.anthropic.
659
+ // claude-…" — the form this repo's own setup guides recommend). None of
660
+ // those match the bare pricing keys ("claude-…"), so every Bedrock call
661
+ // priced to $0. Strip everything up to and including the vendor prefix; the
662
+ // trailing "-v1:0" version suffix is absorbed by the longest-prefix match
663
+ // below. Non-Anthropic Bedrock models (meta./amazon./mistral.) don't match
664
+ // and fall through unchanged, exactly as before.
665
+ const modelKey = stripped === "bedrock" || stripped === "amazonbedrock"
666
+ ? model.replace(/^.*\banthropic\./, "")
667
+ : model;
656
668
  // Exact match
657
- if (providerPricing[model]) {
658
- return providerPricing[model];
669
+ if (providerPricing[modelKey]) {
670
+ return providerPricing[modelKey];
659
671
  }
660
672
  // Longest-prefix match (skip the synthetic "_default" sentinel below)
661
673
  const sortedKeys = Object.keys(providerPricing)
662
674
  .filter((k) => k !== "_default")
663
675
  .sort((a, b) => b.length - a.length);
664
- const key = sortedKeys.find((k) => model.startsWith(k));
676
+ const key = sortedKeys.find((k) => modelKey.startsWith(k));
665
677
  if (key) {
666
678
  return providerPricing[key];
667
679
  }
@@ -44,10 +44,13 @@ export function extractOutputTokens(usage) {
44
44
  * Falls back to input + output if total is not provided
45
45
  */
46
46
  export function extractTotalTokens(usage, input, output) {
47
- if (typeof usage.total === "number") {
47
+ // A literal 0 alongside non-zero components means the provider omitted the
48
+ // figure (several gateways/parsers emit total: 0) — fall through to the
49
+ // computed sum instead of letting the zero win.
50
+ if (typeof usage.total === "number" && usage.total > 0) {
48
51
  return usage.total;
49
52
  }
50
- if (typeof usage.totalTokens === "number") {
53
+ if (typeof usage.totalTokens === "number" && usage.totalTokens > 0) {
51
54
  return usage.totalTokens;
52
55
  }
53
56
  return input + output;
@@ -185,7 +188,6 @@ export function extractTokenUsage(result, options = {}) {
185
188
  // Extract base token counts
186
189
  let input = extractInputTokens(usage);
187
190
  const output = extractOutputTokens(usage);
188
- const total = extractTotalTokens(usage, input, output);
189
191
  // Extract optional token fields
190
192
  const reasoning = extractReasoningTokens(usage);
191
193
  const cacheCreationTokens = extractCacheCreationTokens(usage);
@@ -204,6 +206,39 @@ export function extractTokenUsage(result, options = {}) {
204
206
  input = Math.max(0, input - overlappingCached);
205
207
  }
206
208
  }
209
+ // ai@6-shape rebase: when the cache values came ONLY from the ai@6
210
+ // normalized shape (cachedInputTokens / inputTokenDetails.*), the flat
211
+ // input is cache-INCLUSIVE — asLanguageModelUsage reports
212
+ // inputTokens.total = noCache + cacheRead + cacheWrite — so `input` must
213
+ // be rebased onto the uncached portion or calculateCost bills the cached
214
+ // tokens twice (full input rate + cacheRead/cacheCreation rate). The
215
+ // native fields excluded below (cacheReadInputTokens / cacheReadTokens /
216
+ // cacheCreationInputTokens / cacheCreationTokens) follow the
217
+ // non-overlapping convention where input is already the uncached
218
+ // remainder, and the overlapping snake_case shape was already rebased
219
+ // above — neither must be rebased again.
220
+ if ((cacheReadTokens !== undefined || cacheCreationTokens !== undefined) &&
221
+ usage.cacheReadInputTokens === undefined &&
222
+ usage.cacheReadTokens === undefined &&
223
+ usage.cacheCreationInputTokens === undefined &&
224
+ usage.cacheCreationTokens === undefined &&
225
+ usage.prompt_tokens_details?.cached_tokens === undefined &&
226
+ (usage.cachedInputTokens !== undefined ||
227
+ usage.inputTokenDetails !== undefined)) {
228
+ const noCacheTokens = usage.inputTokenDetails?.noCacheTokens;
229
+ if (typeof noCacheTokens === "number") {
230
+ input = noCacheTokens;
231
+ }
232
+ else {
233
+ input = Math.max(0, input -
234
+ (cacheReadTokens ?? 0) -
235
+ (usage.inputTokenDetails?.cacheWriteTokens ?? 0));
236
+ }
237
+ }
238
+ // Total: prefer the provider-reported figure; the fallback conserves every
239
+ // billed component — cache tokens are additive on top of the (post-rebase)
240
+ // uncached input. Reasoning is already inside `output`, so it is NOT added.
241
+ const total = extractTotalTokens(usage, input + (cacheReadTokens ?? 0) + (cacheCreationTokens ?? 0), output);
207
242
  // Calculate cache savings if enabled
208
243
  const cacheSavingsPercent = calculateCacheSavings
209
244
  ? calculateCacheSavingsPercent(cacheReadTokens, input)
package/dist/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,
@@ -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);
@@ -253,6 +253,8 @@ export class AmazonBedrockProvider extends BaseProvider {
253
253
  let iteration = 0;
254
254
  let totalInputTokens = 0;
255
255
  let totalOutputTokens = 0;
256
+ let totalCacheReadTokens = 0;
257
+ let totalCacheWriteTokens = 0;
256
258
  let lastFinishReason;
257
259
  while (iteration < maxIterations) {
258
260
  iteration++;
@@ -263,8 +265,12 @@ export class AmazonBedrockProvider extends BaseProvider {
263
265
  logger.debug(`[AmazonBedrockProvider] Received Bedrock response`, JSON.stringify(response, null, 2));
264
266
  // Accumulate real token counts and capture the stop reason so
265
267
  // Pipeline B (Langfuse) gets correct usage and finishReason.
268
+ // Converse follows the Anthropic additive convention: inputTokens is
269
+ // the UNCACHED remainder; cache reads/writes are reported separately.
266
270
  totalInputTokens += response.usage?.inputTokens ?? 0;
267
271
  totalOutputTokens += response.usage?.outputTokens ?? 0;
272
+ totalCacheReadTokens += response.usage?.cacheReadInputTokens ?? 0;
273
+ totalCacheWriteTokens += response.usage?.cacheWriteInputTokens ?? 0;
268
274
  if (response.stopReason) {
269
275
  lastFinishReason = response.stopReason;
270
276
  }
@@ -281,7 +287,18 @@ export class AmazonBedrockProvider extends BaseProvider {
281
287
  usage: {
282
288
  input: totalInputTokens,
283
289
  output: totalOutputTokens,
284
- total: totalInputTokens + totalOutputTokens,
290
+ // Cache reads/writes are billed tokens reported separately
291
+ // from inputTokens — the total must include them.
292
+ total: totalInputTokens +
293
+ totalCacheReadTokens +
294
+ totalCacheWriteTokens +
295
+ totalOutputTokens,
296
+ ...(totalCacheReadTokens > 0 && {
297
+ cacheReadTokens: totalCacheReadTokens,
298
+ }),
299
+ ...(totalCacheWriteTokens > 0 && {
300
+ cacheCreationTokens: totalCacheWriteTokens,
301
+ }),
285
302
  },
286
303
  finishReason: lastFinishReason,
287
304
  };
@@ -387,13 +404,26 @@ export class AmazonBedrockProvider extends BaseProvider {
387
404
  const totalDuration = Date.now() - startTime;
388
405
  logger.info(`[AmazonBedrockProvider] Total callBedrock duration: ${totalDuration}ms`);
389
406
  generateSpan.setAttribute("gen_ai.response.stop_reason", response.stopReason ?? "");
390
- generateSpan.setAttribute("gen_ai.usage.input_tokens", response.usage?.inputTokens ?? 0);
407
+ const spanCacheRead = response.usage?.cacheReadInputTokens ?? 0;
408
+ const spanCacheWrite = response.usage?.cacheWriteInputTokens ?? 0;
409
+ // Converse's inputTokens is only the UNCACHED remainder — the span
410
+ // attribute reports the FULL prompt (uncached + cache read/write)
411
+ // so telemetry matches the cache-inclusive pricing inputs below.
412
+ generateSpan.setAttribute("gen_ai.usage.input_tokens", (response.usage?.inputTokens ?? 0) + spanCacheRead + spanCacheWrite);
413
+ generateSpan.setAttribute("gen_ai.usage.cache_read_input_tokens", spanCacheRead);
414
+ generateSpan.setAttribute("gen_ai.usage.cache_creation_input_tokens", spanCacheWrite);
391
415
  generateSpan.setAttribute("gen_ai.usage.output_tokens", response.usage?.outputTokens ?? 0);
392
416
  const cost = calculateCost(this.providerName, this.modelName, {
393
417
  input: response.usage?.inputTokens ?? 0,
394
418
  output: response.usage?.outputTokens ?? 0,
395
419
  total: (response.usage?.inputTokens ?? 0) +
420
+ spanCacheRead +
421
+ spanCacheWrite +
396
422
  (response.usage?.outputTokens ?? 0),
423
+ ...(spanCacheRead > 0 && { cacheReadTokens: spanCacheRead }),
424
+ ...(spanCacheWrite > 0 && {
425
+ cacheCreationTokens: spanCacheWrite,
426
+ }),
397
427
  });
398
428
  if (cost && cost > 0) {
399
429
  generateSpan.setAttribute("neurolink.cost", cost);
@@ -996,7 +1026,9 @@ export class AmazonBedrockProvider extends BaseProvider {
996
1026
  };
997
1027
  return {
998
1028
  stream: asyncIterable,
999
- usage: { total: 0, input: 0, output: 0 },
1029
+ // The generate() fallback already computed real usage — a
1030
+ // hardcoded zero object here threw it away.
1031
+ usage: generateResult.usage,
1000
1032
  model: this.modelName || this.getDefaultModel(),
1001
1033
  provider: this.getProviderName(),
1002
1034
  metadata: {
@@ -1025,6 +1057,8 @@ export class AmazonBedrockProvider extends BaseProvider {
1025
1057
  // so Pipeline B (Langfuse) gets real token counts from Bedrock streams.
1026
1058
  let streamTotalInputTokens = 0;
1027
1059
  let streamTotalOutputTokens = 0;
1060
+ let streamTotalCacheReadTokens = 0;
1061
+ let streamTotalCacheWriteTokens = 0;
1028
1062
  let streamLastStopReason;
1029
1063
  // The REAL issue: ReadableStream errors don't bubble up to the caller
1030
1064
  // So we need to make the first streaming call synchronously to test permissions
@@ -1130,6 +1164,12 @@ export class AmazonBedrockProvider extends BaseProvider {
1130
1164
  chunk.metadata.usage.inputTokens ?? 0;
1131
1165
  streamTotalOutputTokens +=
1132
1166
  chunk.metadata.usage.outputTokens ?? 0;
1167
+ // inputTokens excludes cache reads/writes (Converse follows
1168
+ // the Anthropic additive convention) — track them too.
1169
+ streamTotalCacheReadTokens +=
1170
+ chunk.metadata.usage.cacheReadInputTokens ?? 0;
1171
+ streamTotalCacheWriteTokens +=
1172
+ chunk.metadata.usage.cacheWriteInputTokens ?? 0;
1133
1173
  // Stream is effectively complete after metadata chunk
1134
1174
  break;
1135
1175
  }
@@ -1177,6 +1217,8 @@ export class AmazonBedrockProvider extends BaseProvider {
1177
1217
  if (usage) {
1178
1218
  streamTotalInputTokens += usage.input;
1179
1219
  streamTotalOutputTokens += usage.output;
1220
+ streamTotalCacheReadTokens += usage.cacheReadTokens ?? 0;
1221
+ streamTotalCacheWriteTokens += usage.cacheCreationTokens ?? 0;
1180
1222
  }
1181
1223
  if (stopReason) {
1182
1224
  streamLastStopReason = stopReason;
@@ -1248,7 +1290,18 @@ export class AmazonBedrockProvider extends BaseProvider {
1248
1290
  const aggregatedUsage = {
1249
1291
  input: streamTotalInputTokens,
1250
1292
  output: streamTotalOutputTokens,
1251
- total: streamTotalInputTokens + streamTotalOutputTokens,
1293
+ // Cache reads/writes are billed tokens reported separately
1294
+ // from inputTokens — the total must include them.
1295
+ total: streamTotalInputTokens +
1296
+ streamTotalCacheReadTokens +
1297
+ streamTotalCacheWriteTokens +
1298
+ streamTotalOutputTokens,
1299
+ ...(streamTotalCacheReadTokens > 0 && {
1300
+ cacheReadTokens: streamTotalCacheReadTokens,
1301
+ }),
1302
+ ...(streamTotalCacheWriteTokens > 0 && {
1303
+ cacheCreationTokens: streamTotalCacheWriteTokens,
1304
+ }),
1252
1305
  };
1253
1306
  // Resolve analytics with accumulated token counts from Bedrock
1254
1307
  // metadata chunks so Pipeline A also reports real usage.
@@ -1276,7 +1329,9 @@ export class AmazonBedrockProvider extends BaseProvider {
1276
1329
  };
1277
1330
  return {
1278
1331
  stream: wrappedStreamIterable,
1279
- usage: { total: 0, input: 0, output: 0 },
1332
+ // No usage key here on purpose: the real aggregate resolves through
1333
+ // `analytics` after the stream drains. A literal zero object is
1334
+ // truthy and would block every downstream usage fallback.
1280
1335
  model: this.modelName || this.getDefaultModel(),
1281
1336
  provider: this.getProviderName(),
1282
1337
  analytics: analyticsPromise,
@@ -1468,10 +1523,17 @@ export class AmazonBedrockProvider extends BaseProvider {
1468
1523
  if (chunk.metadata?.usage) {
1469
1524
  const input = chunk.metadata.usage.inputTokens ?? 0;
1470
1525
  const output = chunk.metadata.usage.outputTokens ?? 0;
1526
+ const cacheRead = chunk.metadata.usage.cacheReadInputTokens ?? 0;
1527
+ const cacheWrite = chunk.metadata.usage.cacheWriteInputTokens ?? 0;
1471
1528
  streamUsage = {
1472
1529
  input,
1473
1530
  output,
1474
- total: chunk.metadata.usage.totalTokens ?? input + output,
1531
+ // Computed rather than trusting totalTokens: inputTokens excludes
1532
+ // cache reads/writes (additive convention), and the total must
1533
+ // count every billed component.
1534
+ total: input + cacheRead + cacheWrite + output,
1535
+ ...(cacheRead > 0 && { cacheReadTokens: cacheRead }),
1536
+ ...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
1475
1537
  };
1476
1538
  // Stream is effectively complete after metadata chunk
1477
1539
  break;
@@ -7,6 +7,7 @@ import { ANTHROPIC_TOKEN_URL, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_CLIENT_ID, CLAU
7
7
  import { AnthropicModels, TOKEN_EXPIRY_BUFFER_MS, } from "../constants/enums.js";
8
8
  import { BaseProvider } from "../core/baseProvider.js";
9
9
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
10
+ import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
10
11
  import { getModelCapabilities, getRecommendedModelForTier, isModelAvailableForTier, } from "../models/anthropicModels.js";
11
12
  import { createOAuthFetch } from "../proxy/oauthFetch.js";
12
13
  import { createProxyFetch } from "../proxy/proxyFetch.js";
@@ -1408,13 +1409,24 @@ export class AnthropicProvider extends BaseProvider {
1408
1409
  let capturedProviderError;
1409
1410
  const client = this.client;
1410
1411
  const toolsUsed = [];
1412
+ const streamStartTime = Date.now();
1413
+ // Hoisted out of runLoop so the error path can resolve the usage
1414
+ // accumulated by steps that completed BEFORE the failure — those steps
1415
+ // were billed and must not be reported as zero.
1416
+ let totalInput = 0;
1417
+ let totalOutput = 0;
1418
+ let totalCacheRead = 0;
1419
+ let totalCacheWrite = 0;
1420
+ let lastStop = null;
1421
+ const buildDeferredUsage = () => ({
1422
+ promptTokens: totalInput,
1423
+ completionTokens: totalOutput,
1424
+ totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
1425
+ ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
1426
+ ...(totalCacheWrite > 0 ? { cacheCreationTokens: totalCacheWrite } : {}),
1427
+ });
1411
1428
  const runLoop = async () => {
1412
1429
  const conversation = payload.messages.slice();
1413
- let totalInput = 0;
1414
- let totalOutput = 0;
1415
- let totalCacheRead = 0;
1416
- let totalCacheWrite = 0;
1417
- let lastStop = null;
1418
1430
  for (let step = 0; step < maxSteps; step++) {
1419
1431
  // Mid-turn discovery sync: search_tools (tools.discovery) hydrates
1420
1432
  // new tools into toolsRecord between steps; Claude only calls tools
@@ -1471,10 +1483,20 @@ export class AnthropicProvider extends BaseProvider {
1471
1483
  const thinkingAcc = new Map();
1472
1484
  const toolAcc = new Map();
1473
1485
  let stopReason = null;
1486
+ // message_start carries a small output placeholder and message_delta
1487
+ // reports the CUMULATIVE output for the message — latest wins within
1488
+ // the step (adding both double-counted the placeholder every step).
1489
+ // Write-through: each event folds only the DELTA over this step's
1490
+ // previous value into totalOutput, so the total is correct at every
1491
+ // point mid-drain — a step killed mid-stream (abort/timeout) still
1492
+ // counts the billed output it already reported.
1493
+ let stepOutputTokens = 0;
1474
1494
  for await (const event of events) {
1475
1495
  if (event.type === "message_start") {
1476
1496
  totalInput += event.message.usage.input_tokens ?? 0;
1477
- totalOutput += event.message.usage.output_tokens ?? 0;
1497
+ const startOutputTokens = event.message.usage.output_tokens ?? 0;
1498
+ totalOutput += startOutputTokens - stepOutputTokens;
1499
+ stepOutputTokens = startOutputTokens;
1478
1500
  // Anthropic reports cache reads/writes SEPARATELY from
1479
1501
  // input_tokens on the same message_start event — without these
1480
1502
  // the streaming path silently drops all cache accounting.
@@ -1526,7 +1548,9 @@ export class AnthropicProvider extends BaseProvider {
1526
1548
  }
1527
1549
  else if (event.type === "message_delta") {
1528
1550
  stopReason = event.delta.stop_reason ?? stopReason;
1529
- totalOutput += event.usage?.output_tokens ?? 0;
1551
+ const cumulativeOutputTokens = event.usage?.output_tokens ?? stepOutputTokens;
1552
+ totalOutput += cumulativeOutputTokens - stepOutputTokens;
1553
+ stepOutputTokens = cumulativeOutputTokens;
1530
1554
  }
1531
1555
  }
1532
1556
  lastStop = stopReason;
@@ -1654,15 +1678,7 @@ export class AnthropicProvider extends BaseProvider {
1654
1678
  });
1655
1679
  conversation.push({ role: "user", content: resultBlocks });
1656
1680
  }
1657
- resolveUsage({
1658
- promptTokens: totalInput,
1659
- completionTokens: totalOutput,
1660
- totalTokens: totalInput + totalCacheRead + totalCacheWrite + totalOutput,
1661
- ...(totalCacheRead > 0 ? { cacheReadTokens: totalCacheRead } : {}),
1662
- ...(totalCacheWrite > 0
1663
- ? { cacheCreationTokens: totalCacheWrite }
1664
- : {}),
1665
- });
1681
+ resolveUsage(buildDeferredUsage());
1666
1682
  resolveFinish(lastStop ?? "stop");
1667
1683
  };
1668
1684
  const loopPromise = runLoop()
@@ -1673,6 +1689,9 @@ export class AnthropicProvider extends BaseProvider {
1673
1689
  logger.error("Anthropic: Stream error", {
1674
1690
  error: error instanceof Error ? error.message : String(error),
1675
1691
  });
1692
+ // Report whatever the completed steps accumulated — they were billed
1693
+ // — and unblock any consumer awaiting the usage promise.
1694
+ resolveUsage(buildDeferredUsage());
1676
1695
  resolveFinish("error");
1677
1696
  throw this.formatProviderError(error);
1678
1697
  })
@@ -1734,6 +1753,21 @@ export class AnthropicProvider extends BaseProvider {
1734
1753
  model: this.modelName,
1735
1754
  toolCalls: [],
1736
1755
  toolResults: [],
1756
+ // Wire the deferred usage/finish promises into the analytics collector
1757
+ // (mirrors openaiChatCompletionsBase). Without this the loop computed a
1758
+ // fully correct aggregate that was consumed only by the OTel span —
1759
+ // stream consumers and session cost tracking saw no usage at all.
1760
+ // Chained off finishPromise so requestDuration reflects the DRAINED
1761
+ // stream, not the milliseconds it took to construct this result object.
1762
+ analytics: finishPromise.then(() => streamAnalyticsCollector.createAnalytics(this.providerName, modelId, {
1763
+ textStream: (async function* () { })(),
1764
+ usage: usagePromise,
1765
+ finishReason: finishPromise,
1766
+ }, Date.now() - streamStartTime, {
1767
+ requestId: options.requestId ??
1768
+ `${this.providerName}-stream-${Date.now()}`,
1769
+ streamingMode: true,
1770
+ })),
1737
1771
  };
1738
1772
  }
1739
1773
  async isAvailable() {