@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.
- package/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +318 -318
- package/dist/providers/googleAiStudio/client.js +7 -21
- package/dist/providers/googleNativeGemini3/utils.d.ts +19 -1
- package/dist/providers/googleNativeGemini3/utils.js +45 -0
- package/dist/providers/googleVertex/client.js +150 -125
- package/dist/types/providers.d.ts +8 -0
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
//
|
|
2516
|
-
for
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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
|
-
|
|
2559
|
-
|
|
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
|
-
|
|
2566
|
-
|
|
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[];
|