@juspay/neurolink 10.6.4 → 10.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +6 -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/providers/amazonBedrock.js +68 -6
  21. package/dist/lib/providers/anthropic.js +50 -16
  22. package/dist/lib/providers/googleAiStudio.js +29 -4
  23. package/dist/lib/providers/googleNativeGemini3.js +10 -0
  24. package/dist/lib/providers/googleVertex.js +150 -25
  25. package/dist/lib/providers/litellm.js +13 -2
  26. package/dist/lib/providers/openAI.js +13 -2
  27. package/dist/lib/providers/openaiChatCompletionsBase.js +45 -10
  28. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +2 -14
  29. package/dist/lib/providers/openaiChatCompletionsClient.js +20 -1
  30. package/dist/lib/providers/sagemaker/language-model.js +5 -3
  31. package/dist/lib/providers/sagemaker/parsers.js +18 -9
  32. package/dist/lib/providers/sagemaker/streaming.d.ts +9 -0
  33. package/dist/lib/providers/sagemaker/streaming.js +64 -6
  34. package/dist/lib/types/common.d.ts +3 -0
  35. package/dist/lib/types/openaiCompatible.d.ts +8 -7
  36. package/dist/lib/types/providers.d.ts +6 -0
  37. package/dist/lib/utils/pricing.js +15 -3
  38. package/dist/lib/utils/tokenUtils.js +38 -3
  39. package/dist/neurolink.js +18 -1
  40. package/dist/providers/amazonBedrock.js +68 -6
  41. package/dist/providers/anthropic.js +50 -16
  42. package/dist/providers/googleAiStudio.js +29 -4
  43. package/dist/providers/googleNativeGemini3.js +10 -0
  44. package/dist/providers/googleVertex.js +150 -25
  45. package/dist/providers/litellm.js +13 -2
  46. package/dist/providers/openAI.js +13 -2
  47. package/dist/providers/openaiChatCompletionsBase.js +45 -10
  48. package/dist/providers/openaiChatCompletionsClient.d.ts +2 -14
  49. package/dist/providers/openaiChatCompletionsClient.js +20 -1
  50. package/dist/providers/sagemaker/language-model.js +5 -3
  51. package/dist/providers/sagemaker/parsers.js +18 -9
  52. package/dist/providers/sagemaker/streaming.d.ts +9 -0
  53. package/dist/providers/sagemaker/streaming.js +64 -6
  54. package/dist/types/common.d.ts +3 -0
  55. package/dist/types/openaiCompatible.d.ts +8 -7
  56. package/dist/types/providers.d.ts +6 -0
  57. package/dist/utils/pricing.js +15 -3
  58. package/dist/utils/tokenUtils.js +38 -3
  59. package/package.json +2 -1
@@ -673,9 +673,28 @@ 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
  };
@@ -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
  *
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.6.4",
3
+ "version": "10.6.5",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -104,6 +104,7 @@
104
104
  "test:google-native": "npx tsx test/continuous-test-suite-google-native.ts",
105
105
  "test:gemini-abort": "npx tsx test/continuous-test-suite-gemini-abort.ts",
106
106
  "test:anthropic-cap": "npx tsx test/continuous-test-suite-anthropic-cap.ts",
107
+ "test:token-usage": "npx tsx test/continuous-test-suite-token-usage.ts",
107
108
  "test:tts": "npx tsx test/continuous-test-suite-tts.ts",
108
109
  "test:voice": "npx tsx test/continuous-test-suite-voice.ts",
109
110
  "test:voice-server": "npx tsx test/continuous-test-suite-voice-server.ts",