@juspay/neurolink 11.15.0 → 11.15.1

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.
@@ -575,6 +575,101 @@ function reclaimVertexAnthropicContext(messages, modelName, observedPromptTokens
575
575
  messages.push(...rebuilt);
576
576
  return true;
577
577
  }
578
+ /**
579
+ * Fold one Vertex Gemini step's stream into the shape the loop engine reports.
580
+ *
581
+ * Lifted from the inline drain in executeNativeGemini3Stream so the loop can
582
+ * later be handed to createGeminiLoopAdapter as `collectStep`. Vertex does not
583
+ * share googleNativeGemini3's collector: it reads parts straight off each
584
+ * candidate — avoiding the SDK warning that `chunk.text` raises when
585
+ * thoughtSignature or functionCall parts are present — and that behaviour is
586
+ * characterized.
587
+ *
588
+ * `onUsageDelta` fires PER CHUNK and is not an optimisation to fold away. The
589
+ * drain updates the turn totals incrementally so they are correct at every
590
+ * point mid-stream: a step killed by an abort, the turn deadline or the stall
591
+ * watchdog still bills the tokens it already reported. Returning a step total
592
+ * for the caller to add would be arithmetically identical and operationally
593
+ * wrong, because a killed step never returns.
594
+ */
595
+ async function collectVertexStreamChunks(stream, channel, hooks = {}) {
596
+ const rawResponseParts = [];
597
+ const stepFunctionCalls = [];
598
+ let lastFinishReason;
599
+ let stepInputTokens = 0;
600
+ let stepOutputTokens = 0;
601
+ let stepCacheReadTokens = 0;
602
+ let stepReasoningTokens = 0;
603
+ for await (const chunk of stream) {
604
+ hooks.onProgress?.();
605
+ // Extract raw parts from candidates FIRST
606
+ // This avoids using chunk.text which triggers SDK warning when
607
+ // non-text parts (thoughtSignature, functionCall) are present
608
+ const chunkRecord = chunk;
609
+ const candidates = chunkRecord.candidates;
610
+ const firstCandidate = candidates?.[0];
611
+ // Capture the SDK finish reason (Bug 2: previously dropped). Last
612
+ // non-empty value across chunks wins.
613
+ const chunkFinishReason = firstCandidate?.finishReason;
614
+ if (typeof chunkFinishReason === "string" && chunkFinishReason) {
615
+ lastFinishReason = chunkFinishReason;
616
+ }
617
+ const chunkContent = firstCandidate?.content;
618
+ if (chunkContent && Array.isArray(chunkContent.parts)) {
619
+ for (const part of chunkContent.parts) {
620
+ rawResponseParts.push(part);
621
+ if (typeof part.text === "string" && part.text.length > 0) {
622
+ channel.push({ content: part.text });
623
+ }
624
+ }
625
+ }
626
+ if (chunk.functionCalls) {
627
+ stepFunctionCalls.push(...chunk.functionCalls);
628
+ }
629
+ // Extract usage metadata from chunk
630
+ // promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
631
+ const usageMetadata = chunkRecord.usageMetadata;
632
+ if (usageMetadata) {
633
+ // Take the latest promptTokenCount (usually only in final chunk)
634
+ if (usageMetadata.promptTokenCount !== undefined &&
635
+ usageMetadata.promptTokenCount > 0) {
636
+ hooks.onUsageDelta?.("input", usageMetadata.promptTokenCount - stepInputTokens);
637
+ stepInputTokens = usageMetadata.promptTokenCount;
638
+ // Feed the context guard the REAL prompt size of this call.
639
+ hooks.onUsage?.(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
640
+ // cachedContentTokenCount is OVERLAPPING (a subset already inside
641
+ // promptTokenCount). Clamp to the prompt count so an uncached
642
+ // step reports 0 instead of a stale cached value.
643
+ const chunkCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
644
+ hooks.onUsageDelta?.("cacheRead", chunkCacheReadTokens - stepCacheReadTokens);
645
+ stepCacheReadTokens = chunkCacheReadTokens;
646
+ }
647
+ // Take the latest candidatesTokenCount (accumulates through chunks)
648
+ if (usageMetadata.candidatesTokenCount !== undefined &&
649
+ usageMetadata.candidatesTokenCount > 0) {
650
+ hooks.onUsageDelta?.("output", usageMetadata.candidatesTokenCount - stepOutputTokens);
651
+ stepOutputTokens = usageMetadata.candidatesTokenCount;
652
+ }
653
+ // thoughtsTokenCount (thinking tokens, billed at the output
654
+ // rate) is NOT part of candidatesTokenCount — Gemini reports
655
+ // totalTokenCount = prompt + candidates + thoughts.
656
+ if (usageMetadata.thoughtsTokenCount !== undefined &&
657
+ usageMetadata.thoughtsTokenCount > 0) {
658
+ hooks.onUsageDelta?.("reasoning", usageMetadata.thoughtsTokenCount - stepReasoningTokens);
659
+ stepReasoningTokens = usageMetadata.thoughtsTokenCount;
660
+ }
661
+ }
662
+ }
663
+ return {
664
+ rawResponseParts,
665
+ stepFunctionCalls,
666
+ ...(lastFinishReason ? { finishReason: lastFinishReason } : {}),
667
+ inputTokens: stepInputTokens,
668
+ outputTokens: stepOutputTokens,
669
+ ...(stepCacheReadTokens ? { cacheReadTokens: stepCacheReadTokens } : {}),
670
+ ...(stepReasoningTokens ? { reasoningTokens: stepReasoningTokens } : {}),
671
+ };
672
+ }
578
673
  export class GoogleVertexProvider extends BaseProvider {
579
674
  projectId;
580
675
  location;
@@ -1515,74 +1610,39 @@ export class GoogleVertexProvider extends BaseProvider {
1515
1610
  // are therefore correct at every point mid-drain — a step killed
1516
1611
  // mid-stream (abort / turn deadline / stall watchdog) still counts
1517
1612
  // the billed tokens it already reported.
1518
- let stepInputTokens = 0;
1519
- let stepOutputTokens = 0;
1520
- let stepCacheReadTokens = 0;
1521
- let stepReasoningTokens = 0;
1522
- for await (const chunk of stream) {
1523
- turnClock.noteProgress();
1524
- // Extract raw parts from candidates FIRST
1525
- // This avoids using chunk.text which triggers SDK warning when
1526
- // non-text parts (thoughtSignature, functionCall) are present
1527
- const chunkRecord = chunk;
1528
- const candidates = chunkRecord.candidates;
1529
- const firstCandidate = candidates?.[0];
1530
- // Capture the SDK finish reason (Bug 2: previously dropped). Last
1531
- // non-empty value across chunks wins.
1532
- const chunkFinishReason = firstCandidate?.finishReason;
1533
- if (typeof chunkFinishReason === "string" && chunkFinishReason) {
1534
- lastFinishReason = chunkFinishReason;
1535
- stepFinishReason = chunkFinishReason;
1536
- }
1537
- const chunkContent = firstCandidate?.content;
1538
- if (chunkContent && Array.isArray(chunkContent.parts)) {
1539
- for (const part of chunkContent.parts) {
1540
- rawResponseParts.push(part);
1541
- if (typeof part.text === "string" && part.text.length > 0) {
1542
- incrementalTextChunks.push(part.text);
1543
- }
1613
+ // The drain now lives in collectVertexStreamChunks. The hooks below
1614
+ // are the two couplings it had to this loop, plus the per-chunk
1615
+ // usage deltas — those must stay per-chunk so a step killed
1616
+ // mid-stream still bills what it reported.
1617
+ const collected = await collectVertexStreamChunks(stream, {
1618
+ push: (chunk) => {
1619
+ if (chunk.content) {
1620
+ incrementalTextChunks.push(chunk.content);
1544
1621
  }
1545
- }
1546
- if (chunk.functionCalls) {
1547
- stepFunctionCalls.push(...chunk.functionCalls);
1548
- }
1549
- // Extract usage metadata from chunk
1550
- // promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
1551
- const usageMetadata = chunkRecord.usageMetadata;
1552
- if (usageMetadata) {
1553
- // Take the latest promptTokenCount (usually only in final chunk)
1554
- if (usageMetadata.promptTokenCount !== undefined &&
1555
- usageMetadata.promptTokenCount > 0) {
1556
- totalInputTokens +=
1557
- usageMetadata.promptTokenCount - stepInputTokens;
1558
- stepInputTokens = usageMetadata.promptTokenCount;
1559
- // Feed the context guard the REAL prompt size of this call.
1560
- contextGuard.noteUsage(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
1561
- // cachedContentTokenCount is OVERLAPPING (a subset already inside
1562
- // promptTokenCount). Clamp to the prompt count so an uncached
1563
- // step reports 0 instead of a stale cached value.
1564
- const chunkCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
1565
- totalCacheReadTokens +=
1566
- chunkCacheReadTokens - stepCacheReadTokens;
1567
- stepCacheReadTokens = chunkCacheReadTokens;
1622
+ },
1623
+ }, {
1624
+ onProgress: () => turnClock.noteProgress(),
1625
+ onUsage: (input, output) => contextGuard.noteUsage(input, output),
1626
+ onUsageDelta: (counter, delta) => {
1627
+ if (counter === "input") {
1628
+ totalInputTokens += delta;
1568
1629
  }
1569
- // Take the latest candidatesTokenCount (accumulates through chunks)
1570
- if (usageMetadata.candidatesTokenCount !== undefined &&
1571
- usageMetadata.candidatesTokenCount > 0) {
1572
- totalOutputTokens +=
1573
- usageMetadata.candidatesTokenCount - stepOutputTokens;
1574
- stepOutputTokens = usageMetadata.candidatesTokenCount;
1630
+ else if (counter === "output") {
1631
+ totalOutputTokens += delta;
1575
1632
  }
1576
- // thoughtsTokenCount (thinking tokens, billed at the output
1577
- // rate) is NOT part of candidatesTokenCount — Gemini reports
1578
- // totalTokenCount = prompt + candidates + thoughts.
1579
- if (usageMetadata.thoughtsTokenCount !== undefined &&
1580
- usageMetadata.thoughtsTokenCount > 0) {
1581
- totalReasoningTokens +=
1582
- usageMetadata.thoughtsTokenCount - stepReasoningTokens;
1583
- stepReasoningTokens = usageMetadata.thoughtsTokenCount;
1633
+ else if (counter === "cacheRead") {
1634
+ totalCacheReadTokens += delta;
1584
1635
  }
1585
- }
1636
+ else {
1637
+ totalReasoningTokens += delta;
1638
+ }
1639
+ },
1640
+ });
1641
+ rawResponseParts.push(...collected.rawResponseParts);
1642
+ stepFunctionCalls.push(...collected.stepFunctionCalls);
1643
+ if (collected.finishReason) {
1644
+ stepFinishReason = collected.finishReason;
1645
+ lastFinishReason = collected.finishReason;
1586
1646
  }
1587
1647
  // Extract text from raw parts after stream completes
1588
1648
  // This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
@@ -2508,70 +2568,35 @@ export class GoogleVertexProvider extends BaseProvider {
2508
2568
  // are therefore correct at every point mid-drain — a step killed
2509
2569
  // mid-stream (abort / turn deadline / stall watchdog) still counts
2510
2570
  // the billed tokens it already reported.
2511
- let stepInputTokens = 0;
2512
- let stepOutputTokens = 0;
2513
- let stepCacheReadTokens = 0;
2514
- let stepReasoningTokens = 0;
2515
- // Collect all chunks from stream
2516
- for await (const chunk of stream) {
2517
- turnClock.noteProgress();
2518
- // Extract raw parts from candidates FIRST
2519
- // This avoids using chunk.text which triggers SDK warning when
2520
- // non-text parts (thoughtSignature, functionCall) are present
2521
- const chunkRecord = chunk;
2522
- const candidates = chunkRecord.candidates;
2523
- const firstCandidate = candidates?.[0];
2524
- // Capture the SDK finish reason (Bug 2: previously dropped). Last
2525
- // non-empty value across chunks wins.
2526
- const chunkFinishReason = firstCandidate?.finishReason;
2527
- if (typeof chunkFinishReason === "string" && chunkFinishReason) {
2528
- lastFinishReason = chunkFinishReason;
2529
- stepFinishReason = chunkFinishReason;
2530
- }
2531
- const chunkContent = firstCandidate?.content;
2532
- if (chunkContent && Array.isArray(chunkContent.parts)) {
2533
- rawResponseParts.push(...chunkContent.parts);
2534
- }
2535
- if (chunk.functionCalls) {
2536
- stepFunctionCalls.push(...chunk.functionCalls);
2537
- }
2538
- // Extract usage metadata from chunk
2539
- // promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
2540
- const usageMetadata = chunkRecord.usageMetadata;
2541
- if (usageMetadata) {
2542
- // Take the latest promptTokenCount (usually only in final chunk)
2543
- if (usageMetadata.promptTokenCount !== undefined &&
2544
- usageMetadata.promptTokenCount > 0) {
2545
- totalInputTokens +=
2546
- usageMetadata.promptTokenCount - stepInputTokens;
2547
- stepInputTokens = usageMetadata.promptTokenCount;
2548
- // Feed the context guard the REAL prompt size of this call.
2549
- contextGuard.noteUsage(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
2550
- // cachedContentTokenCount is OVERLAPPING (a subset already inside
2551
- // promptTokenCount). Clamp to the prompt count so an uncached
2552
- // step reports 0 instead of a stale cached value.
2553
- const chunkCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
2554
- totalCacheReadTokens +=
2555
- chunkCacheReadTokens - stepCacheReadTokens;
2556
- stepCacheReadTokens = chunkCacheReadTokens;
2571
+ // Same collector as the streaming twin. generate() returns one
2572
+ // result rather than streaming, so the channel is a no-op — the
2573
+ // stream loop uses it to fill incrementalTextChunks for replay, and
2574
+ // there is nothing to replay here. Everything else is identical,
2575
+ // including the per-chunk usage deltas that keep the turn totals
2576
+ // correct for a step killed mid-stream.
2577
+ const collected = await collectVertexStreamChunks(stream, { push: () => { } }, {
2578
+ onProgress: () => turnClock.noteProgress(),
2579
+ onUsage: (input, output) => contextGuard.noteUsage(input, output),
2580
+ onUsageDelta: (counter, delta) => {
2581
+ if (counter === "input") {
2582
+ totalInputTokens += delta;
2557
2583
  }
2558
- // Take the latest candidatesTokenCount (accumulates through chunks)
2559
- if (usageMetadata.candidatesTokenCount !== undefined &&
2560
- usageMetadata.candidatesTokenCount > 0) {
2561
- totalOutputTokens +=
2562
- usageMetadata.candidatesTokenCount - stepOutputTokens;
2563
- stepOutputTokens = usageMetadata.candidatesTokenCount;
2584
+ else if (counter === "output") {
2585
+ totalOutputTokens += delta;
2564
2586
  }
2565
- // thoughtsTokenCount (thinking tokens, billed at the output
2566
- // rate) is NOT part of candidatesTokenCount — Gemini reports
2567
- // totalTokenCount = prompt + candidates + thoughts.
2568
- if (usageMetadata.thoughtsTokenCount !== undefined &&
2569
- usageMetadata.thoughtsTokenCount > 0) {
2570
- totalReasoningTokens +=
2571
- usageMetadata.thoughtsTokenCount - stepReasoningTokens;
2572
- stepReasoningTokens = usageMetadata.thoughtsTokenCount;
2587
+ else if (counter === "cacheRead") {
2588
+ totalCacheReadTokens += delta;
2573
2589
  }
2574
- }
2590
+ else {
2591
+ totalReasoningTokens += delta;
2592
+ }
2593
+ },
2594
+ });
2595
+ rawResponseParts.push(...collected.rawResponseParts);
2596
+ stepFunctionCalls.push(...collected.stepFunctionCalls);
2597
+ if (collected.finishReason) {
2598
+ stepFinishReason = collected.finishReason;
2599
+ lastFinishReason = collected.finishReason;
2575
2600
  }
2576
2601
  // Extract text from raw parts after stream completes
2577
2602
  // This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
@@ -1735,6 +1735,14 @@ export type NativeFunctionResponse = {
1735
1735
  };
1736
1736
  };
1737
1737
  /** Result from collectStreamChunks. */
1738
+ /**
1739
+ * Which turn-level counter a per-chunk Vertex usage delta belongs to.
1740
+ *
1741
+ * Vertex updates its turn totals incrementally so they stay correct mid-stream
1742
+ * — a step killed by an abort, the turn deadline or the stall watchdog still
1743
+ * bills the tokens it already reported.
1744
+ */
1745
+ export type VertexUsageCounter = "input" | "output" | "cacheRead" | "reasoning";
1738
1746
  export type CollectedChunkResult = {
1739
1747
  rawResponseParts: unknown[];
1740
1748
  stepFunctionCalls: NativeFunctionCall[];