@juspay/neurolink 9.26.0 → 9.26.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 +12 -0
- package/dist/adapters/providerImageAdapter.js +6 -0
- package/dist/constants/contextWindows.js +2 -0
- package/dist/constants/enums.d.ts +2 -0
- package/dist/constants/enums.js +2 -0
- package/dist/lib/adapters/providerImageAdapter.js +6 -0
- package/dist/lib/constants/contextWindows.js +2 -0
- package/dist/lib/constants/enums.d.ts +2 -0
- package/dist/lib/constants/enums.js +2 -0
- package/dist/lib/providers/googleAiStudio.js +135 -89
- package/dist/lib/providers/googleNativeGemini3.d.ts +43 -0
- package/dist/lib/providers/googleNativeGemini3.js +148 -18
- package/dist/lib/providers/googleVertex.js +162 -140
- package/dist/providers/googleAiStudio.js +135 -89
- package/dist/providers/googleNativeGemini3.d.ts +43 -0
- package/dist/providers/googleNativeGemini3.js +148 -18
- package/dist/providers/googleVertex.js +162 -140
- package/package.json +18 -17
|
@@ -11,7 +11,7 @@ import { logger } from "../utils/logger.js";
|
|
|
11
11
|
import { isGemini3Model } from "../utils/modelDetection.js";
|
|
12
12
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
13
13
|
import { estimateTokens } from "../utils/tokenEstimation.js";
|
|
14
|
-
import { buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, computeMaxSteps, executeNativeToolCalls, extractTextFromParts, handleMaxStepsTermination, pushModelResponseToHistory, sanitizeToolsForGemini, } from "./googleNativeGemini3.js";
|
|
14
|
+
import { buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createTextChannel, executeNativeToolCalls, extractTextFromParts, handleMaxStepsTermination, pushModelResponseToHistory, sanitizeToolsForGemini, } from "./googleNativeGemini3.js";
|
|
15
15
|
// Google AI Live API types now imported from ../types/providerSpecific.js
|
|
16
16
|
// Import proper types for multimodal message handling
|
|
17
17
|
// Create Google GenAI client
|
|
@@ -578,107 +578,151 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
578
578
|
}
|
|
579
579
|
const config = buildNativeConfig(options, toolsConfig);
|
|
580
580
|
const maxSteps = computeMaxSteps(options.maxSteps);
|
|
581
|
-
let finalText = "";
|
|
582
|
-
let lastStepText = "";
|
|
583
|
-
let totalInputTokens = 0;
|
|
584
|
-
let totalOutputTokens = 0;
|
|
585
|
-
const allToolCalls = [];
|
|
586
|
-
let step = 0;
|
|
587
|
-
const failedTools = new Map();
|
|
588
581
|
// Compose abort signal from user signal + timeout
|
|
589
582
|
const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
583
|
+
// Create a push-based text channel so the caller receives tokens as
|
|
584
|
+
// they arrive from the network rather than after full buffering.
|
|
585
|
+
const channel = createTextChannel();
|
|
586
|
+
// Shared mutable state updated by the background agentic loop.
|
|
587
|
+
const allToolCalls = [];
|
|
588
|
+
// analyticsResolvers lets the background loop settle the analytics
|
|
589
|
+
// promise once token counts are known (after the loop completes).
|
|
590
|
+
let analyticsResolve;
|
|
591
|
+
let analyticsReject;
|
|
592
|
+
const analyticsPromise = new Promise((res, rej) => {
|
|
593
|
+
analyticsResolve = res;
|
|
594
|
+
analyticsReject = rej;
|
|
595
|
+
});
|
|
596
|
+
// Shared metadata object mutated by the background loop so the
|
|
597
|
+
// returned object reflects the final values after stream completion.
|
|
598
|
+
const metadata = {
|
|
599
|
+
streamId: `native-${Date.now()}`,
|
|
600
|
+
startTime,
|
|
601
|
+
responseTime: 0,
|
|
602
|
+
totalToolExecutions: 0,
|
|
603
|
+
};
|
|
604
|
+
// Run the agentic loop in the background without awaiting it here,
|
|
605
|
+
// so we can return the StreamResult (with channel.iterable) immediately.
|
|
606
|
+
const loopPromise = (async () => {
|
|
607
|
+
let lastStepText = "";
|
|
608
|
+
let totalInputTokens = 0;
|
|
609
|
+
let totalOutputTokens = 0;
|
|
610
|
+
let step = 0;
|
|
611
|
+
let completedWithFinalAnswer = false;
|
|
612
|
+
const failedTools = new Map();
|
|
599
613
|
try {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
614
|
+
// Agentic loop for tool calling
|
|
615
|
+
while (step < maxSteps) {
|
|
616
|
+
if (composedSignal?.aborted) {
|
|
617
|
+
throw composedSignal.reason instanceof Error
|
|
618
|
+
? composedSignal.reason
|
|
619
|
+
: new Error("Request aborted");
|
|
620
|
+
}
|
|
621
|
+
step++;
|
|
622
|
+
logger.debug(`[GoogleAIStudio] Native SDK step ${step}/${maxSteps}`);
|
|
623
|
+
try {
|
|
624
|
+
const rawStream = await client.models.generateContentStream({
|
|
625
|
+
model: modelName,
|
|
626
|
+
contents: currentContents,
|
|
627
|
+
config,
|
|
628
|
+
...(composedSignal
|
|
629
|
+
? { httpOptions: { signal: composedSignal } }
|
|
630
|
+
: {}),
|
|
631
|
+
});
|
|
632
|
+
// For every step, use incremental collection so text parts
|
|
633
|
+
// are pushed to the channel as they arrive. For intermediate
|
|
634
|
+
// steps (those that produce function calls) we still need the
|
|
635
|
+
// complete rawResponseParts for pushModelResponseToHistory,
|
|
636
|
+
// which collectStreamChunksIncremental provides at stream end.
|
|
637
|
+
const chunkResult = await collectStreamChunksIncremental(rawStream, channel);
|
|
638
|
+
totalInputTokens += chunkResult.inputTokens;
|
|
639
|
+
totalOutputTokens += chunkResult.outputTokens;
|
|
640
|
+
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
641
|
+
// If no function calls, this was the final step — channel
|
|
642
|
+
// already received all text parts incrementally.
|
|
643
|
+
if (chunkResult.stepFunctionCalls.length === 0) {
|
|
644
|
+
completedWithFinalAnswer = true;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
lastStepText = stepText;
|
|
648
|
+
// Record tool call events on the span
|
|
649
|
+
for (const fc of chunkResult.stepFunctionCalls) {
|
|
650
|
+
span.addEvent("gen_ai.tool_call", {
|
|
651
|
+
"tool.name": fc.name,
|
|
652
|
+
"tool.step": step,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
logger.debug(`[GoogleAIStudio] Executing ${chunkResult.stepFunctionCalls.length} function calls`);
|
|
656
|
+
// Add model response with ALL parts (including thoughtSignature) to history
|
|
657
|
+
pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
|
|
658
|
+
const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, { abortSignal: composedSignal });
|
|
659
|
+
// Add function responses to history — the @google/genai SDK
|
|
660
|
+
// only accepts "user" and "model" as valid roles in contents.
|
|
661
|
+
// Function/tool responses must use role: "user" (matching the
|
|
662
|
+
// SDK's own automaticFunctionCalling implementation).
|
|
663
|
+
currentContents.push({
|
|
664
|
+
role: "user",
|
|
665
|
+
parts: functionResponses,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
logger.error("[GoogleAIStudio] Native SDK error", error);
|
|
670
|
+
throw this.handleProviderError(error);
|
|
671
|
+
}
|
|
616
672
|
}
|
|
617
|
-
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
673
|
+
// Handle max-steps termination: if the model was still calling
|
|
674
|
+
// tools when we hit the limit, push a synthetic final message.
|
|
675
|
+
const hitStepLimitWithoutFinalAnswer = step >= maxSteps && !completedWithFinalAnswer;
|
|
676
|
+
if (hitStepLimitWithoutFinalAnswer) {
|
|
677
|
+
const fallback = handleMaxStepsTermination("[GoogleAIStudio]", step, maxSteps, "", // finalText is empty — model didn't stop on its own
|
|
678
|
+
lastStepText);
|
|
679
|
+
if (fallback) {
|
|
680
|
+
channel.push(fallback);
|
|
681
|
+
}
|
|
624
682
|
}
|
|
625
|
-
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
683
|
+
const responseTime = Date.now() - startTime;
|
|
684
|
+
// Update shared metadata so the returned object reflects final values.
|
|
685
|
+
metadata.responseTime = responseTime;
|
|
686
|
+
metadata.totalToolExecutions = allToolCalls.length;
|
|
687
|
+
// Set token usage and finish reason on the span
|
|
688
|
+
span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
|
|
689
|
+
span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
|
|
690
|
+
span.setAttribute(ATTR.GEN_AI_FINISH_REASON, hitStepLimitWithoutFinalAnswer ? "max_steps" : "stop");
|
|
691
|
+
analyticsResolve({
|
|
692
|
+
provider: this.providerName,
|
|
693
|
+
model: modelName,
|
|
694
|
+
tokenUsage: {
|
|
695
|
+
input: totalInputTokens,
|
|
696
|
+
output: totalOutputTokens,
|
|
697
|
+
total: totalInputTokens + totalOutputTokens,
|
|
698
|
+
},
|
|
699
|
+
requestDuration: responseTime,
|
|
700
|
+
timestamp: new Date().toISOString(),
|
|
636
701
|
});
|
|
702
|
+
channel.close();
|
|
637
703
|
}
|
|
638
|
-
catch (
|
|
639
|
-
|
|
640
|
-
|
|
704
|
+
catch (err) {
|
|
705
|
+
channel.error(err);
|
|
706
|
+
analyticsReject(err);
|
|
641
707
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// Create async iterable for streaming result
|
|
650
|
-
async function* createTextStream() {
|
|
651
|
-
yield { content: finalText };
|
|
652
|
-
}
|
|
708
|
+
finally {
|
|
709
|
+
timeoutController?.cleanup();
|
|
710
|
+
}
|
|
711
|
+
})();
|
|
712
|
+
// Suppress unhandled-rejection warnings on loopPromise — errors are
|
|
713
|
+
// forwarded to the channel and will surface when the caller iterates.
|
|
714
|
+
loopPromise.catch(() => undefined);
|
|
653
715
|
return {
|
|
654
|
-
stream:
|
|
716
|
+
stream: channel.iterable,
|
|
655
717
|
provider: this.providerName,
|
|
656
718
|
model: modelName,
|
|
657
|
-
toolCalls: allToolCalls
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
})),
|
|
661
|
-
analytics: Promise.resolve({
|
|
662
|
-
provider: this.providerName,
|
|
663
|
-
model: modelName,
|
|
664
|
-
tokenUsage: {
|
|
665
|
-
input: totalInputTokens,
|
|
666
|
-
output: totalOutputTokens,
|
|
667
|
-
total: totalInputTokens + totalOutputTokens,
|
|
668
|
-
},
|
|
669
|
-
requestDuration: responseTime,
|
|
670
|
-
timestamp: new Date().toISOString(),
|
|
671
|
-
}),
|
|
672
|
-
metadata: {
|
|
673
|
-
streamId: `native-${Date.now()}`,
|
|
674
|
-
startTime,
|
|
675
|
-
responseTime,
|
|
676
|
-
totalToolExecutions: allToolCalls.length,
|
|
677
|
-
},
|
|
719
|
+
toolCalls: allToolCalls,
|
|
720
|
+
analytics: analyticsPromise,
|
|
721
|
+
metadata,
|
|
678
722
|
};
|
|
679
723
|
}
|
|
680
724
|
finally {
|
|
681
|
-
|
|
725
|
+
// Timeout controller cleanup is managed inside the background loop
|
|
682
726
|
}
|
|
683
727
|
});
|
|
684
728
|
}
|
|
@@ -709,7 +753,9 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
709
753
|
hasTools: !!options.tools && Object.keys(options.tools).length > 0,
|
|
710
754
|
});
|
|
711
755
|
// Build contents from input
|
|
712
|
-
|
|
756
|
+
// Prefer input.text over prompt — processCSVFilesForNativeSDK enriches
|
|
757
|
+
// input.text with inlined CSV data, so using prompt first would discard it.
|
|
758
|
+
const promptText = options.input?.text || options.prompt || "";
|
|
713
759
|
const currentContents = [{ role: "user", parts: [{ text: promptText }] }];
|
|
714
760
|
// Convert tools (merge SDK tools with options.tools)
|
|
715
761
|
let toolsConfig;
|
|
@@ -98,6 +98,49 @@ export declare function collectStreamChunks(stream: AsyncIterable<{
|
|
|
98
98
|
functionCalls?: NativeFunctionCall[];
|
|
99
99
|
[key: string]: unknown;
|
|
100
100
|
}>): Promise<CollectedChunkResult>;
|
|
101
|
+
/**
|
|
102
|
+
* A push-based text channel that decouples producers (agentic loop) from
|
|
103
|
+
* consumers (the caller's async iterable).
|
|
104
|
+
*
|
|
105
|
+
* The producer calls `push(text)` for each chunk and `close()` / `error(err)`
|
|
106
|
+
* when done. The consumer iterates the `iterable` async generator.
|
|
107
|
+
*/
|
|
108
|
+
export type TextChannel = {
|
|
109
|
+
/** Push a text chunk to the consumer. */
|
|
110
|
+
push(text: string): void;
|
|
111
|
+
/** Signal that no more chunks will arrive. */
|
|
112
|
+
close(): void;
|
|
113
|
+
/** Signal that the producer encountered a fatal error. */
|
|
114
|
+
error(err: unknown): void;
|
|
115
|
+
/** Async iterable consumed by the StreamResult. */
|
|
116
|
+
iterable: AsyncIterable<{
|
|
117
|
+
content: string;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Create a push-based text channel that bridges a background producer
|
|
122
|
+
* (the agentic tool-calling loop) with an async-iterable consumer.
|
|
123
|
+
*
|
|
124
|
+
* This enables truly incremental streaming: text parts are yielded to the
|
|
125
|
+
* caller as they arrive from the network, rather than being buffered until
|
|
126
|
+
* the model finishes generating.
|
|
127
|
+
*/
|
|
128
|
+
export declare function createTextChannel(): TextChannel;
|
|
129
|
+
/**
|
|
130
|
+
* Iterate a single stream step incrementally, pushing text parts to `channel`
|
|
131
|
+
* as they arrive from the network while simultaneously accumulating the full
|
|
132
|
+
* `CollectedChunkResult` needed for history and token accounting.
|
|
133
|
+
*
|
|
134
|
+
* Used for all steps (both intermediate tool-calling steps and the final
|
|
135
|
+
* text-only step). Text parts are pushed to the channel as they arrive,
|
|
136
|
+
* enabling truly incremental streaming. The complete `rawResponseParts`
|
|
137
|
+
* (including thoughtSignature) are still returned at the end for use by
|
|
138
|
+
* `pushModelResponseToHistory`.
|
|
139
|
+
*/
|
|
140
|
+
export declare function collectStreamChunksIncremental(stream: AsyncIterable<{
|
|
141
|
+
functionCalls?: NativeFunctionCall[];
|
|
142
|
+
[key: string]: unknown;
|
|
143
|
+
}>, channel: TextChannel): Promise<CollectedChunkResult>;
|
|
101
144
|
/**
|
|
102
145
|
* Extract text from raw response parts, filtering out non-text parts
|
|
103
146
|
* (thoughtSignature, functionCall) to avoid SDK warnings.
|
|
@@ -158,7 +158,6 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
158
158
|
logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
159
|
// Don't fall back to the original tool — an incompatible schema would fail the Gemini request
|
|
160
160
|
dropped.push(name);
|
|
161
|
-
continue;
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
163
|
return { tools: sanitized, dropped };
|
|
@@ -171,29 +170,45 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
171
170
|
export function buildNativeToolDeclarations(tools) {
|
|
172
171
|
const functionDeclarations = [];
|
|
173
172
|
const executeMap = new Map();
|
|
173
|
+
const skippedTools = [];
|
|
174
174
|
for (const [name, tool] of Object.entries(tools)) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
175
|
+
try {
|
|
176
|
+
const decl = {
|
|
177
|
+
name,
|
|
178
|
+
description: tool.description || `Tool: ${name}`,
|
|
179
|
+
};
|
|
180
|
+
if (tool.parameters) {
|
|
181
|
+
let rawSchema;
|
|
182
|
+
if (isZodSchema(tool.parameters)) {
|
|
183
|
+
rawSchema = convertZodToJsonSchema(tool.parameters);
|
|
184
|
+
}
|
|
185
|
+
else if (typeof tool.parameters === "object") {
|
|
186
|
+
rawSchema = tool.parameters;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
rawSchema = { type: "object", properties: {} };
|
|
190
|
+
}
|
|
191
|
+
// Unwrap Vercel AI SDK's jsonSchema() wrapper: { jsonSchema: { type: "object", ... } }
|
|
192
|
+
if (rawSchema.jsonSchema &&
|
|
193
|
+
typeof rawSchema.jsonSchema === "object" &&
|
|
194
|
+
!rawSchema.type) {
|
|
195
|
+
rawSchema = rawSchema.jsonSchema;
|
|
196
|
+
}
|
|
197
|
+
decl.parametersJsonSchema = sanitizeSchemaForGemini(inlineJsonSchema(rawSchema));
|
|
186
198
|
}
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
functionDeclarations.push(decl);
|
|
200
|
+
if (tool.execute) {
|
|
201
|
+
executeMap.set(name, tool.execute);
|
|
189
202
|
}
|
|
190
|
-
decl.parametersJsonSchema = sanitizeSchemaForGemini(inlineJsonSchema(rawSchema));
|
|
191
203
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
catch (err) {
|
|
205
|
+
skippedTools.push(name);
|
|
206
|
+
logger.error(`[buildNativeToolDeclarations] Failed to convert tool "${name}":`, err);
|
|
195
207
|
}
|
|
196
208
|
}
|
|
209
|
+
if (skippedTools.length > 0) {
|
|
210
|
+
logger.warn(`[buildNativeToolDeclarations] ${skippedTools.length} tool(s) skipped due to schema errors: ${skippedTools.join(", ")}`);
|
|
211
|
+
}
|
|
197
212
|
return { toolsConfig: [{ functionDeclarations }], executeMap };
|
|
198
213
|
}
|
|
199
214
|
/**
|
|
@@ -265,6 +280,121 @@ export async function collectStreamChunks(stream) {
|
|
|
265
280
|
}
|
|
266
281
|
return { rawResponseParts, stepFunctionCalls, inputTokens, outputTokens };
|
|
267
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Create a push-based text channel that bridges a background producer
|
|
285
|
+
* (the agentic tool-calling loop) with an async-iterable consumer.
|
|
286
|
+
*
|
|
287
|
+
* This enables truly incremental streaming: text parts are yielded to the
|
|
288
|
+
* caller as they arrive from the network, rather than being buffered until
|
|
289
|
+
* the model finishes generating.
|
|
290
|
+
*/
|
|
291
|
+
export function createTextChannel() {
|
|
292
|
+
const queue = [];
|
|
293
|
+
let done = false;
|
|
294
|
+
let fatalError = undefined;
|
|
295
|
+
// Resolve the current "wait for data" promise when new data arrives
|
|
296
|
+
let notify = null;
|
|
297
|
+
function wake() {
|
|
298
|
+
if (notify) {
|
|
299
|
+
const fn = notify;
|
|
300
|
+
notify = null;
|
|
301
|
+
fn();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function push(text) {
|
|
305
|
+
if (done) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
queue.push({ content: text });
|
|
309
|
+
wake();
|
|
310
|
+
}
|
|
311
|
+
function close() {
|
|
312
|
+
done = true;
|
|
313
|
+
wake();
|
|
314
|
+
}
|
|
315
|
+
function error(err) {
|
|
316
|
+
done = true;
|
|
317
|
+
fatalError = err;
|
|
318
|
+
wake();
|
|
319
|
+
}
|
|
320
|
+
let readIndex = 0;
|
|
321
|
+
async function* iterable() {
|
|
322
|
+
try {
|
|
323
|
+
while (true) {
|
|
324
|
+
if (readIndex < queue.length) {
|
|
325
|
+
yield queue[readIndex++];
|
|
326
|
+
// Periodically compact consumed chunks to avoid unbounded retention
|
|
327
|
+
if (readIndex > 1024 && readIndex * 2 >= queue.length) {
|
|
328
|
+
queue.splice(0, readIndex);
|
|
329
|
+
readIndex = 0;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (done) {
|
|
333
|
+
if (fatalError !== undefined) {
|
|
334
|
+
throw fatalError instanceof Error
|
|
335
|
+
? fatalError
|
|
336
|
+
: new Error(String(fatalError));
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// Wait until the producer pushes data or signals completion
|
|
342
|
+
await new Promise((resolve) => {
|
|
343
|
+
notify = resolve;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
// Consumer stopped reading (e.g. disconnect/cancel): stop buffering.
|
|
350
|
+
done = true;
|
|
351
|
+
queue.length = 0;
|
|
352
|
+
notify?.();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { push, close, error, iterable: iterable() };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Iterate a single stream step incrementally, pushing text parts to `channel`
|
|
359
|
+
* as they arrive from the network while simultaneously accumulating the full
|
|
360
|
+
* `CollectedChunkResult` needed for history and token accounting.
|
|
361
|
+
*
|
|
362
|
+
* Used for all steps (both intermediate tool-calling steps and the final
|
|
363
|
+
* text-only step). Text parts are pushed to the channel as they arrive,
|
|
364
|
+
* enabling truly incremental streaming. The complete `rawResponseParts`
|
|
365
|
+
* (including thoughtSignature) are still returned at the end for use by
|
|
366
|
+
* `pushModelResponseToHistory`.
|
|
367
|
+
*/
|
|
368
|
+
export async function collectStreamChunksIncremental(stream, channel) {
|
|
369
|
+
const rawResponseParts = [];
|
|
370
|
+
const stepFunctionCalls = [];
|
|
371
|
+
let inputTokens = 0;
|
|
372
|
+
let outputTokens = 0;
|
|
373
|
+
for await (const chunk of stream) {
|
|
374
|
+
const chunkRecord = chunk;
|
|
375
|
+
const candidates = chunkRecord.candidates;
|
|
376
|
+
const firstCandidate = candidates?.[0];
|
|
377
|
+
const chunkContent = firstCandidate?.content;
|
|
378
|
+
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
379
|
+
for (const part of chunkContent.parts) {
|
|
380
|
+
rawResponseParts.push(part);
|
|
381
|
+
// Forward text parts to the consumer immediately
|
|
382
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
383
|
+
channel.push(part.text);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (chunk.functionCalls) {
|
|
388
|
+
stepFunctionCalls.push(...chunk.functionCalls);
|
|
389
|
+
}
|
|
390
|
+
const usage = chunkRecord.usageMetadata;
|
|
391
|
+
if (usage) {
|
|
392
|
+
inputTokens = Math.max(inputTokens, usage.promptTokenCount || 0);
|
|
393
|
+
outputTokens = Math.max(outputTokens, usage.candidatesTokenCount || 0);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { rawResponseParts, stepFunctionCalls, inputTokens, outputTokens };
|
|
397
|
+
}
|
|
268
398
|
/**
|
|
269
399
|
* Extract text from raw response parts, filtering out non-text parts
|
|
270
400
|
* (thoughtSignature, functionCall) to avoid SDK warnings.
|