@juspay/neurolink 11.15.0 → 11.15.2

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.
@@ -17,7 +17,7 @@ import { withTimeout } from "../../utils/async/index.js";
17
17
  import { estimateTokens } from "../../utils/tokenEstimation.js";
18
18
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
19
19
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
20
- import { buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
20
+ import { buildDedupedEngineTools, buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
21
21
  import { createStreamChannel } from "../../core/streamChannel.js";
22
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
23
23
  import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
@@ -835,16 +835,10 @@ export class GoogleAIStudioProvider extends BaseProvider {
835
835
  return next;
836
836
  },
837
837
  };
838
- const engineTools = {};
839
- for (const [name, tool] of Object.entries(options.tools ?? {})) {
840
- const execute = tool?.execute;
841
- if (!execute) {
842
- continue;
843
- }
844
- engineTools[name] = {
845
- execute: async (args, opts) => execute(args, opts),
846
- };
847
- }
838
+ // Through the turn's DedupExecuteMap, NOT the raw executors:
839
+ // `.get()` returns the dedup wrapper that answers an identical
840
+ // repeated {name, args} from the per-turn cache (BZ-3327).
841
+ const engineTools = buildDedupedEngineTools(declarationsResult, options.tools);
848
842
  const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
849
843
  tools: engineTools,
850
844
  ...(composedSignal ? { abortSignal: composedSignal } : {}),
@@ -1151,16 +1145,8 @@ export class GoogleAIStudioProvider extends BaseProvider {
1151
1145
  return next;
1152
1146
  },
1153
1147
  };
1154
- const engineTools = {};
1155
- for (const [name, tool] of Object.entries(options.tools ?? {})) {
1156
- const execute = tool?.execute;
1157
- if (!execute) {
1158
- continue;
1159
- }
1160
- engineTools[name] = {
1161
- execute: async (args, opts) => execute(args, opts),
1162
- };
1163
- }
1148
+ // Same dedup routing as the streaming twin above.
1149
+ const engineTools = buildDedupedEngineTools(declarationsResult, options.tools);
1164
1150
  const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
1165
1151
  tools: engineTools,
1166
1152
  ...(composedSignal ? { abortSignal: composedSignal } : {}),
@@ -8,7 +8,7 @@
8
8
  * This module extracts the functions that are duplicated between the two
9
9
  * providers so they can share a single implementation.
10
10
  */
11
- import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
11
+ import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
12
12
  import type { Tool } from "../../types/index.js";
13
13
  /**
14
14
  * A per-turn tool execute map that deduplicates identical tool calls.
@@ -91,6 +91,24 @@ export declare function buildNativeToolDeclarations(tools: Record<string, Tool>,
91
91
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
92
92
  * `toolsConfig` by reference — and returns true when anything was added.
93
93
  */
94
+ /**
95
+ * Build the tool record handed to `runAgenticLoop`, routed through the turn's
96
+ * DedupExecuteMap.
97
+ *
98
+ * The engine looks tools up by the name the adapter reports, which is the
99
+ * ORIGINAL caller-facing name; `executeMap` is keyed by the SANITIZED wire
100
+ * name Google actually declares. `originalNameMap` is the bridge, and it
101
+ * carries an entry for every converted tool (identity mappings included), so
102
+ * iterating it yields exactly the declared, executable set.
103
+ *
104
+ * Going through `executeMap.get()` rather than the raw `tool.execute` is the
105
+ * entire point: `.get()` returns the dedup wrapper, so an identical
106
+ * {name, args} repeated within one turn is answered from the per-turn cache
107
+ * instead of running the tool again (BZ-3327). Passing the raw executor looks
108
+ * identical in every test that calls a tool once, and silently reintroduces
109
+ * duplicate side effects the moment the model repeats itself.
110
+ */
111
+ export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined): NonNullable<AgenticLoopOptions["tools"]>;
94
112
  export declare function refreshNativeToolDeclarations(liveTools: Record<string, Tool> | undefined, current: NativeToolDeclarationsResult): boolean;
95
113
  /**
96
114
  * Build the native @google/genai config object shared by stream and generate.
@@ -455,6 +455,51 @@ export function buildNativeToolDeclarations(tools, reservedNames) {
455
455
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
456
456
  * `toolsConfig` by reference — and returns true when anything was added.
457
457
  */
458
+ /**
459
+ * Build the tool record handed to `runAgenticLoop`, routed through the turn's
460
+ * DedupExecuteMap.
461
+ *
462
+ * The engine looks tools up by the name the adapter reports, which is the
463
+ * ORIGINAL caller-facing name; `executeMap` is keyed by the SANITIZED wire
464
+ * name Google actually declares. `originalNameMap` is the bridge, and it
465
+ * carries an entry for every converted tool (identity mappings included), so
466
+ * iterating it yields exactly the declared, executable set.
467
+ *
468
+ * Going through `executeMap.get()` rather than the raw `tool.execute` is the
469
+ * entire point: `.get()` returns the dedup wrapper, so an identical
470
+ * {name, args} repeated within one turn is answered from the per-turn cache
471
+ * instead of running the tool again (BZ-3327). Passing the raw executor looks
472
+ * identical in every test that calls a tool once, and silently reintroduces
473
+ * duplicate side effects the moment the model repeats itself.
474
+ */
475
+ export function buildDedupedEngineTools(declarations, tools) {
476
+ const engineTools = {};
477
+ if (declarations) {
478
+ for (const [safeName, originalName] of declarations.originalNameMap) {
479
+ const execute = declarations.executeMap.get(safeName);
480
+ if (!execute) {
481
+ continue;
482
+ }
483
+ engineTools[originalName] = {
484
+ execute: async (args, opts) => execute(args, opts),
485
+ };
486
+ }
487
+ return engineTools;
488
+ }
489
+ // No declarations were built (no tools, or a path that skips the snapshot).
490
+ // Fall back to the caller's own executors so this helper can never REMOVE a
491
+ // tool that would otherwise have been callable.
492
+ for (const [name, tool] of Object.entries(tools ?? {})) {
493
+ const execute = tool?.execute;
494
+ if (!execute) {
495
+ continue;
496
+ }
497
+ engineTools[name] = {
498
+ execute: async (args, opts) => execute(args, opts),
499
+ };
500
+ }
501
+ return engineTools;
502
+ }
458
503
  export function refreshNativeToolDeclarations(liveTools, current) {
459
504
  if (!liveTools) {
460
505
  return false;
@@ -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[];