@juspay/neurolink 10.10.6 → 10.10.8
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 +15 -0
- package/README.md +37 -8
- package/dist/browser/neurolink.min.js +399 -399
- package/dist/cli/factories/commandFactory.js +8 -4
- package/dist/constants/contextWindows.js +10 -1
- package/dist/context/anthropicLoopGuard.d.ts +1 -0
- package/dist/context/anthropicLoopGuard.js +30 -12
- package/dist/context/contextCompactor.js +19 -0
- package/dist/context/geminiLoopGuard.d.ts +54 -0
- package/dist/context/geminiLoopGuard.js +140 -0
- package/dist/core/redisConversationMemoryManager.d.ts +27 -0
- package/dist/core/redisConversationMemoryManager.js +146 -25
- package/dist/lib/constants/contextWindows.js +10 -1
- package/dist/lib/context/anthropicLoopGuard.d.ts +1 -0
- package/dist/lib/context/anthropicLoopGuard.js +30 -12
- package/dist/lib/context/contextCompactor.js +19 -0
- package/dist/lib/context/geminiLoopGuard.d.ts +54 -0
- package/dist/lib/context/geminiLoopGuard.js +141 -0
- package/dist/lib/core/redisConversationMemoryManager.d.ts +27 -0
- package/dist/lib/core/redisConversationMemoryManager.js +146 -25
- package/dist/lib/processors/media/VideoProcessor.d.ts +13 -3
- package/dist/lib/processors/media/VideoProcessor.js +53 -12
- package/dist/lib/providers/googleAiStudio/client.d.ts +0 -31
- package/dist/lib/providers/googleAiStudio/client.js +118 -1
- package/dist/lib/providers/googleNativeGemini3/utils.d.ts +9 -0
- package/dist/lib/providers/googleNativeGemini3/utils.js +12 -0
- package/dist/lib/providers/googleVertex/client.d.ts +0 -45
- package/dist/lib/providers/googleVertex/client.js +201 -21
- package/dist/lib/types/context.d.ts +9 -0
- package/dist/lib/types/file.d.ts +37 -0
- package/dist/lib/types/generate.d.ts +4 -0
- package/dist/lib/types/stream.d.ts +4 -0
- package/dist/lib/utils/errorHandling.d.ts +21 -0
- package/dist/lib/utils/errorHandling.js +53 -0
- package/dist/lib/utils/fileDetector.js +9 -6
- package/dist/lib/utils/messageBuilder.js +111 -36
- package/dist/lib/utils/pdfProcessor.d.ts +11 -0
- package/dist/lib/utils/pdfProcessor.js +17 -0
- package/dist/lib/utils/redis.d.ts +60 -1
- package/dist/lib/utils/redis.js +143 -12
- package/dist/processors/media/VideoProcessor.d.ts +13 -3
- package/dist/processors/media/VideoProcessor.js +53 -12
- package/dist/providers/googleAiStudio/client.d.ts +0 -31
- package/dist/providers/googleAiStudio/client.js +118 -1
- package/dist/providers/googleNativeGemini3/utils.d.ts +9 -0
- package/dist/providers/googleNativeGemini3/utils.js +12 -0
- package/dist/providers/googleVertex/client.d.ts +0 -45
- package/dist/providers/googleVertex/client.js +201 -21
- package/dist/types/context.d.ts +9 -0
- package/dist/types/file.d.ts +37 -0
- package/dist/types/generate.d.ts +4 -0
- package/dist/types/stream.d.ts +4 -0
- package/dist/utils/errorHandling.d.ts +21 -0
- package/dist/utils/errorHandling.js +53 -0
- package/dist/utils/fileDetector.js +9 -6
- package/dist/utils/messageBuilder.js +111 -36
- package/dist/utils/pdfProcessor.d.ts +11 -0
- package/dist/utils/pdfProcessor.js +17 -0
- package/dist/utils/redis.d.ts +60 -1
- package/dist/utils/redis.js +143 -12
- package/package.json +3 -1
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
47
|
-
import type { FileInfo, ProcessedVideo, ProcessorFileProcessingResult, ProcessOptions } from "../../types/index.js";
|
|
47
|
+
import type { FileInfo, ProcessedVideo, ProcessorFileProcessingResult, ProcessOptions, VideoProcessorOptions } from "../../types/index.js";
|
|
48
48
|
/**
|
|
49
49
|
* Narrow a loaded `fluent-ffmpeg` export to the shape this file actually uses:
|
|
50
50
|
* a callable carrying the `ffprobe` and `setFfmpegPath` statics.
|
|
@@ -116,7 +116,7 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
|
|
|
116
116
|
* @param options - Optional processing options
|
|
117
117
|
* @returns Processing result with extracted video data or error
|
|
118
118
|
*/
|
|
119
|
-
processFile(fileInfo: FileInfo, options?: ProcessOptions): Promise<ProcessorFileProcessingResult<ProcessedVideo>>;
|
|
119
|
+
processFile(fileInfo: FileInfo, options?: ProcessOptions & VideoProcessorOptions): Promise<ProcessorFileProcessingResult<ProcessedVideo>>;
|
|
120
120
|
/**
|
|
121
121
|
* Probe a video file to extract metadata using ffprobe.
|
|
122
122
|
*
|
|
@@ -137,6 +137,11 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
|
|
|
137
137
|
* @returns Structured video metadata
|
|
138
138
|
*/
|
|
139
139
|
private buildMetadata;
|
|
140
|
+
/**
|
|
141
|
+
* Clamp a caller-supplied frame quality into sharp's valid 1-100 range,
|
|
142
|
+
* falling back to the default when absent or non-numeric (#478).
|
|
143
|
+
*/
|
|
144
|
+
private static resolveFrameQuality;
|
|
140
145
|
/**
|
|
141
146
|
* Extract keyframes from a video at calculated intervals.
|
|
142
147
|
*
|
|
@@ -153,10 +158,15 @@ export declare class VideoProcessor extends BaseFileProcessor<ProcessedVideo> {
|
|
|
153
158
|
* The interval is adaptive: if the tier interval would exceed MAX_FRAMES,
|
|
154
159
|
* the interval widens to duration/MAX_FRAMES for full-video coverage.
|
|
155
160
|
*
|
|
161
|
+
* A caller-supplied `options.frames` overrides the tier schedule entirely:
|
|
162
|
+
* that many frames are spread evenly across the clip, still capped at
|
|
163
|
+
* MAX_FRAMES. `options.quality` and `options.format` reach the encoder (#478).
|
|
164
|
+
*
|
|
156
165
|
* @param videoPath - Path to the video file
|
|
157
166
|
* @param tempDir - Temp directory for frame output
|
|
158
167
|
* @param durationSec - Video duration in seconds
|
|
159
|
-
* @
|
|
168
|
+
* @param options - Caller frame budget / encoder settings
|
|
169
|
+
* @returns Array of encoded frame buffers (JPEG unless png was requested)
|
|
160
170
|
*/
|
|
161
171
|
private extractKeyframes;
|
|
162
172
|
/**
|
|
@@ -313,7 +313,10 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
313
313
|
* @param options - Optional processing options
|
|
314
314
|
* @returns Processing result with extracted video data or error
|
|
315
315
|
*/
|
|
316
|
-
async processFile(fileInfo,
|
|
316
|
+
async processFile(fileInfo,
|
|
317
|
+
// #478: widened with the keyframe knobs so `--video-frames`/`-quality`/
|
|
318
|
+
// `-format` can reach the encoder instead of being silently discarded.
|
|
319
|
+
options) {
|
|
317
320
|
const filename = this.getFilename(fileInfo);
|
|
318
321
|
const sizeBytes = fileInfo.size || fileInfo.buffer?.length || 0;
|
|
319
322
|
return withSpan({
|
|
@@ -438,7 +441,7 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
438
441
|
// Step 5: Extract keyframes
|
|
439
442
|
let keyframes = [];
|
|
440
443
|
try {
|
|
441
|
-
keyframes = await this.extractKeyframes(tempVideoPath, tempDir, metadata.duration);
|
|
444
|
+
keyframes = await this.extractKeyframes(tempVideoPath, tempDir, metadata.duration, options);
|
|
442
445
|
}
|
|
443
446
|
catch {
|
|
444
447
|
// Non-fatal: continue without keyframes if extraction fails
|
|
@@ -642,6 +645,16 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
642
645
|
// ===========================================================================
|
|
643
646
|
// KEYFRAME EXTRACTION
|
|
644
647
|
// ===========================================================================
|
|
648
|
+
/**
|
|
649
|
+
* Clamp a caller-supplied frame quality into sharp's valid 1-100 range,
|
|
650
|
+
* falling back to the default when absent or non-numeric (#478).
|
|
651
|
+
*/
|
|
652
|
+
static resolveFrameQuality(quality) {
|
|
653
|
+
if (typeof quality !== "number" || !Number.isFinite(quality)) {
|
|
654
|
+
return VIDEO_CONFIG.FRAME_JPEG_QUALITY;
|
|
655
|
+
}
|
|
656
|
+
return Math.min(100, Math.max(1, Math.round(quality)));
|
|
657
|
+
}
|
|
645
658
|
/**
|
|
646
659
|
* Extract keyframes from a video at calculated intervals.
|
|
647
660
|
*
|
|
@@ -658,20 +671,45 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
658
671
|
* The interval is adaptive: if the tier interval would exceed MAX_FRAMES,
|
|
659
672
|
* the interval widens to duration/MAX_FRAMES for full-video coverage.
|
|
660
673
|
*
|
|
674
|
+
* A caller-supplied `options.frames` overrides the tier schedule entirely:
|
|
675
|
+
* that many frames are spread evenly across the clip, still capped at
|
|
676
|
+
* MAX_FRAMES. `options.quality` and `options.format` reach the encoder (#478).
|
|
677
|
+
*
|
|
661
678
|
* @param videoPath - Path to the video file
|
|
662
679
|
* @param tempDir - Temp directory for frame output
|
|
663
680
|
* @param durationSec - Video duration in seconds
|
|
664
|
-
* @
|
|
681
|
+
* @param options - Caller frame budget / encoder settings
|
|
682
|
+
* @returns Array of encoded frame buffers (JPEG unless png was requested)
|
|
665
683
|
*/
|
|
666
|
-
async extractKeyframes(videoPath, tempDir, durationSec) {
|
|
684
|
+
async extractKeyframes(videoPath, tempDir, durationSec, options) {
|
|
667
685
|
if (durationSec <= 0) {
|
|
668
686
|
return [];
|
|
669
687
|
}
|
|
670
|
-
//
|
|
671
|
-
|
|
688
|
+
// #478: honor the caller's frame budget, still bounded by MAX_FRAMES so a
|
|
689
|
+
// CLI flag can lower the cost but never raise it past the processor's own
|
|
690
|
+
// ceiling. A non-positive/non-finite request falls back to the default.
|
|
691
|
+
const requestedFrames = options?.frames;
|
|
692
|
+
const hasExplicitBudget = typeof requestedFrames === "number" &&
|
|
693
|
+
Number.isFinite(requestedFrames) &&
|
|
694
|
+
requestedFrames > 0;
|
|
695
|
+
const frameBudget = hasExplicitBudget
|
|
696
|
+
? Math.min(Math.floor(requestedFrames), VIDEO_CONFIG.MAX_FRAMES)
|
|
697
|
+
: VIDEO_CONFIG.MAX_FRAMES;
|
|
698
|
+
// Determine extraction interval based on duration. When the caller asked
|
|
699
|
+
// for a specific frame count, spread that many evenly across the whole
|
|
700
|
+
// video instead of using the duration tier — otherwise a short interval
|
|
701
|
+
// would hit the budget early and only cover the opening seconds.
|
|
702
|
+
//
|
|
703
|
+
// Keyed on whether a budget was REQUESTED, not on whether it happens to be
|
|
704
|
+
// below MAX_FRAMES: asking for exactly MAX_FRAMES is still an explicit
|
|
705
|
+
// request and must produce that many frames, not silently fall back to the
|
|
706
|
+
// tier schedule (which yields far fewer on a short clip).
|
|
707
|
+
const intervalSec = hasExplicitBudget
|
|
708
|
+
? Math.max(durationSec / frameBudget, Number.EPSILON)
|
|
709
|
+
: this.getFrameInterval(durationSec);
|
|
672
710
|
// Calculate timestamps to extract
|
|
673
711
|
const timestamps = [];
|
|
674
|
-
for (let t = 0; t < durationSec && timestamps.length <
|
|
712
|
+
for (let t = 0; t < durationSec && timestamps.length < frameBudget; t += intervalSec) {
|
|
675
713
|
timestamps.push(t);
|
|
676
714
|
}
|
|
677
715
|
if (timestamps.length === 0) {
|
|
@@ -691,13 +729,16 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
691
729
|
const rawFrame = await fs.readFile(framePath);
|
|
692
730
|
// Resize to fit within max dimension while preserving aspect ratio
|
|
693
731
|
const sharp = (await import("sharp")).default;
|
|
694
|
-
const
|
|
695
|
-
.resize(VIDEO_CONFIG.FRAME_MAX_DIMENSION, VIDEO_CONFIG.FRAME_MAX_DIMENSION, {
|
|
732
|
+
const pipeline = sharp(rawFrame).resize(VIDEO_CONFIG.FRAME_MAX_DIMENSION, VIDEO_CONFIG.FRAME_MAX_DIMENSION, {
|
|
696
733
|
fit: "inside",
|
|
697
734
|
withoutEnlargement: true,
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
|
|
735
|
+
});
|
|
736
|
+
// #478: `--video-quality` / `--video-format` were accepted by the CLI
|
|
737
|
+
// and then dropped on the floor; both now reach the encoder.
|
|
738
|
+
const quality = VideoProcessor.resolveFrameQuality(options?.quality);
|
|
739
|
+
const resized = await (options?.format === "png"
|
|
740
|
+
? pipeline.png({ quality })
|
|
741
|
+
: pipeline.jpeg({ quality })).toBuffer();
|
|
701
742
|
keyframes.push(resized);
|
|
702
743
|
}
|
|
703
744
|
catch {
|
|
@@ -2,37 +2,6 @@ import { type AIProviderName } from "../../constants/enums.js";
|
|
|
2
2
|
import { BaseProvider } from "../../core/baseProvider.js";
|
|
3
3
|
import type { ZodUnknownSchema, EnhancedGenerateResult, TextGenerationOptions, StreamOptions, StreamResult } from "../../types/index.js";
|
|
4
4
|
import type { LanguageModel, Schema } from "../../types/index.js";
|
|
5
|
-
/**
|
|
6
|
-
* Google AI Studio provider implementation using BaseProvider
|
|
7
|
-
* Migrated from original GoogleAIStudio class to new factory pattern
|
|
8
|
-
*
|
|
9
|
-
* @important Structured Output Limitation
|
|
10
|
-
* Google Gemini models cannot combine function calling (tools) with structured
|
|
11
|
-
* output (JSON schema). When using schemas with output.format: "json", you MUST
|
|
12
|
-
* set disableTools: true.
|
|
13
|
-
*
|
|
14
|
-
* Error without disableTools:
|
|
15
|
-
* "Function calling with a response mime type: 'application/json' is unsupported"
|
|
16
|
-
*
|
|
17
|
-
* This is a Google API limitation documented at:
|
|
18
|
-
* https://ai.google.dev/gemini-api/docs/function-calling
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* // ✅ Correct usage with schemas
|
|
23
|
-
* const provider = new GoogleAIStudioProvider("gemini-2.5-flash");
|
|
24
|
-
* const result = await provider.generate({
|
|
25
|
-
* input: { text: "Analyze data" },
|
|
26
|
-
* schema: MySchema,
|
|
27
|
-
* output: { format: "json" },
|
|
28
|
-
* disableTools: true // Required
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* @note Gemini 3 Pro Preview (November 2025) will support combining tools + schemas
|
|
33
|
-
* @note "Too many states for serving" errors can occur with complex schemas + tools.
|
|
34
|
-
* Solution: Simplify schema or use disableTools: true
|
|
35
|
-
*/
|
|
36
5
|
export declare class GoogleAIStudioProvider extends BaseProvider {
|
|
37
6
|
private credentials?;
|
|
38
7
|
constructor(modelName?: string, sdk?: unknown, credentials?: {
|
|
@@ -6,12 +6,14 @@ import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "
|
|
|
6
6
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
7
7
|
import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
+
import { GEMINI_ELISION_NOTE, planGeminiLoopReclaim, previewGeminiToolResponseText, } from "../../context/geminiLoopGuard.js";
|
|
10
|
+
import { getAvailableInputTokens, getContextWindowSize, } from "../../constants/contextWindows.js";
|
|
9
11
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../../utils/timeout.js";
|
|
10
12
|
import { withTimeout } from "../../utils/async/index.js";
|
|
11
13
|
import { estimateTokens } from "../../utils/tokenEstimation.js";
|
|
12
14
|
import { transformToolExecutions } from "../../utils/transformationUtils.js";
|
|
13
15
|
import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
|
|
14
|
-
import { buildGeminiResponseSchema, buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createTextChannel, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
|
|
16
|
+
import { buildGeminiResponseSchema, buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createContextGuard, createTextChannel, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
|
|
15
17
|
import { createProxyFetch } from "../../proxy/proxyFetch.js";
|
|
16
18
|
// Google AI Live API types now imported from ../types/providerSpecific.js
|
|
17
19
|
// Import proper types for multimodal message handling
|
|
@@ -69,6 +71,74 @@ async function createGoogleGenAIClient(apiKey) {
|
|
|
69
71
|
* @note "Too many states for serving" errors can occur with complex schemas + tools.
|
|
70
72
|
* Solution: Simplify schema or use disableTools: true
|
|
71
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* Reclaim context from an AI Studio loop history IN PLACE.
|
|
76
|
+
*
|
|
77
|
+
* This loop had NO in-turn guard at all — it appended a model turn plus a tool
|
|
78
|
+
* turn every step with nothing bounding growth, so a long agentic run walked
|
|
79
|
+
* into a provider "context length exceeded" and lost every completed step.
|
|
80
|
+
* Shares its reclaim policy with the other provider loops via loopGuardCore.
|
|
81
|
+
*
|
|
82
|
+
* Returns true when something was reclaimed.
|
|
83
|
+
*/
|
|
84
|
+
function reclaimAiStudioContext(contents, modelName, observedPromptTokens) {
|
|
85
|
+
const plan = planGeminiLoopReclaim({
|
|
86
|
+
contents,
|
|
87
|
+
availableInputTokens: getAvailableInputTokens("googleAiStudio", modelName),
|
|
88
|
+
provider: "googleAiStudio",
|
|
89
|
+
...(observedPromptTokens ? { observedPromptTokens } : {}),
|
|
90
|
+
});
|
|
91
|
+
if (!plan) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const dropSet = new Set(plan.drop);
|
|
95
|
+
const truncateSet = new Set(plan.truncate);
|
|
96
|
+
const rebuilt = [];
|
|
97
|
+
for (let i = 0; i < contents.length; i++) {
|
|
98
|
+
if (dropSet.has(i)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const content = contents[i];
|
|
102
|
+
if (truncateSet.has(i) && Array.isArray(content.parts)) {
|
|
103
|
+
rebuilt.push({
|
|
104
|
+
...content,
|
|
105
|
+
parts: content.parts.map((part) => {
|
|
106
|
+
const record = part;
|
|
107
|
+
if (!record.functionResponse) {
|
|
108
|
+
return part;
|
|
109
|
+
}
|
|
110
|
+
const text = JSON.stringify(record.functionResponse.response) ?? "";
|
|
111
|
+
if (text.length <= 2048) {
|
|
112
|
+
return part;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
functionResponse: {
|
|
116
|
+
name: record.functionResponse.name,
|
|
117
|
+
response: { result: previewGeminiToolResponseText(text) },
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
rebuilt.push(content);
|
|
125
|
+
}
|
|
126
|
+
if (dropSet.size > 0) {
|
|
127
|
+
let noteIndex = rebuilt.findIndex((c) => Array.isArray(c.parts) &&
|
|
128
|
+
c.parts.some((part) => !!part.functionCall ||
|
|
129
|
+
!!part.functionResponse));
|
|
130
|
+
if (noteIndex < 0) {
|
|
131
|
+
noteIndex = Math.min(1, rebuilt.length);
|
|
132
|
+
}
|
|
133
|
+
rebuilt.splice(noteIndex, 0, {
|
|
134
|
+
role: "user",
|
|
135
|
+
parts: [{ text: GEMINI_ELISION_NOTE }],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
contents.length = 0;
|
|
139
|
+
contents.push(...rebuilt);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
72
142
|
export class GoogleAIStudioProvider extends BaseProvider {
|
|
73
143
|
credentials;
|
|
74
144
|
constructor(modelName, sdk, credentials) {
|
|
@@ -595,9 +665,27 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
595
665
|
let step = 0;
|
|
596
666
|
let completedWithFinalAnswer = false;
|
|
597
667
|
const failedTools = new Map();
|
|
668
|
+
// Cheap trigger for the in-turn reclaim, mirroring the Vertex twin.
|
|
669
|
+
// Planning serializes the WHOLE accumulated history to estimate it,
|
|
670
|
+
// so running it unconditionally charges that once per step for the
|
|
671
|
+
// life of the turn; the guard tracks real prompt counts plus
|
|
672
|
+
// measured growth instead, and it supplies the observed count that
|
|
673
|
+
// calibrates the planner's char estimate.
|
|
674
|
+
const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
|
|
598
675
|
try {
|
|
599
676
|
// Agentic loop for tool calling
|
|
600
677
|
while (step < maxSteps) {
|
|
678
|
+
// In-turn context guard: this loop appends a model turn plus a
|
|
679
|
+
// tool turn every step with nothing bounding growth. No-op
|
|
680
|
+
// while the request still fits, so a loop that fits never pays
|
|
681
|
+
// a cache invalidation. Step 0 still plans unconditionally —
|
|
682
|
+
// the guard has no usage to go on yet, and the incoming history
|
|
683
|
+
// can already be oversized before the first call.
|
|
684
|
+
if (step === 0 || contextGuard.shouldStop()) {
|
|
685
|
+
if (reclaimAiStudioContext(currentContents, modelName, contextGuard.projectedNextPromptTokens)) {
|
|
686
|
+
contextGuard.resetAfterReclaim();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
601
689
|
if (composedSignal?.aborted) {
|
|
602
690
|
throw composedSignal.reason instanceof Error
|
|
603
691
|
? composedSignal.reason
|
|
@@ -629,6 +717,10 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
629
717
|
totalOutputTokens += chunkResult.outputTokens;
|
|
630
718
|
totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
|
|
631
719
|
totalReasoningTokens += chunkResult.reasoningTokens ?? 0;
|
|
720
|
+
// `inputTokens` is this step's promptTokenCount — the FULL
|
|
721
|
+
// prompt size for the request just made, which is what the
|
|
722
|
+
// guard projects the next request from.
|
|
723
|
+
contextGuard.noteUsage(chunkResult.inputTokens, chunkResult.outputTokens);
|
|
632
724
|
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
633
725
|
// If no function calls, this was the final step — channel
|
|
634
726
|
// already received all text parts incrementally.
|
|
@@ -691,6 +783,14 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
691
783
|
role: "user",
|
|
692
784
|
parts: functionResponses,
|
|
693
785
|
});
|
|
786
|
+
// Project this step's growth: the appended tool results ride
|
|
787
|
+
// the next prompt, which the provider has not reported on yet.
|
|
788
|
+
try {
|
|
789
|
+
contextGuard.noteAppendedChars(JSON.stringify(functionResponses).length);
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
/* estimation is best-effort — never break the loop */
|
|
793
|
+
}
|
|
694
794
|
}
|
|
695
795
|
catch (error) {
|
|
696
796
|
logger.error("[GoogleAIStudio] Native SDK error", error);
|
|
@@ -877,8 +977,16 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
877
977
|
const toolExecutions = [];
|
|
878
978
|
let step = 0;
|
|
879
979
|
const failedTools = new Map();
|
|
980
|
+
// Cheap reclaim trigger — see the stream twin.
|
|
981
|
+
const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
|
|
880
982
|
// Agentic loop for tool calling
|
|
881
983
|
while (step < maxSteps) {
|
|
984
|
+
// In-turn context guard — see the stream twin.
|
|
985
|
+
if (step === 0 || contextGuard.shouldStop()) {
|
|
986
|
+
if (reclaimAiStudioContext(currentContents, modelName, contextGuard.projectedNextPromptTokens)) {
|
|
987
|
+
contextGuard.resetAfterReclaim();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
882
990
|
if (composedSignal?.aborted) {
|
|
883
991
|
throw composedSignal.reason instanceof Error
|
|
884
992
|
? composedSignal.reason
|
|
@@ -904,6 +1012,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
904
1012
|
totalOutputTokens += chunkResult.outputTokens;
|
|
905
1013
|
totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
|
|
906
1014
|
totalReasoningTokens += chunkResult.reasoningTokens ?? 0;
|
|
1015
|
+
contextGuard.noteUsage(chunkResult.inputTokens, chunkResult.outputTokens);
|
|
907
1016
|
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
908
1017
|
// If no function calls, we're done
|
|
909
1018
|
if (chunkResult.stepFunctionCalls.length === 0) {
|
|
@@ -961,6 +1070,14 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
961
1070
|
role: "user",
|
|
962
1071
|
parts: functionResponses,
|
|
963
1072
|
});
|
|
1073
|
+
// Project this step's growth: the appended tool results ride
|
|
1074
|
+
// the next prompt, which the provider has not reported on yet.
|
|
1075
|
+
try {
|
|
1076
|
+
contextGuard.noteAppendedChars(JSON.stringify(functionResponses).length);
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
/* estimation is best-effort — never break the loop */
|
|
1080
|
+
}
|
|
964
1081
|
}
|
|
965
1082
|
catch (error) {
|
|
966
1083
|
logger.error("[GoogleAIStudio] Native SDK generate error", error);
|
|
@@ -385,6 +385,15 @@ export declare function createContextGuard(contextWindowTokens: number, threshol
|
|
|
385
385
|
* results, nudge text) using the ~4 chars/token heuristic.
|
|
386
386
|
*/
|
|
387
387
|
noteAppendedChars(chars: number): void;
|
|
388
|
+
/**
|
|
389
|
+
* Clear the projection after the caller has reclaimed context.
|
|
390
|
+
*
|
|
391
|
+
* The observed prompt size reflects the pre-reclaim conversation, so
|
|
392
|
+
* leaving it in place would keep `shouldStop()` true forever and defeat
|
|
393
|
+
* the reclaim. Resetting to the fail-open state means the guard stays
|
|
394
|
+
* quiet until the next real usage report re-establishes the truth.
|
|
395
|
+
*/
|
|
396
|
+
resetAfterReclaim(): void;
|
|
388
397
|
/** True when issuing another model call risks crossing the threshold. */
|
|
389
398
|
shouldStop(): boolean;
|
|
390
399
|
};
|
|
@@ -1229,6 +1229,18 @@ export function createContextGuard(contextWindowTokens, thresholdRatio = DEFAULT
|
|
|
1229
1229
|
projectedGrowthTokens += Math.ceil(chars / 4);
|
|
1230
1230
|
}
|
|
1231
1231
|
},
|
|
1232
|
+
/**
|
|
1233
|
+
* Clear the projection after the caller has reclaimed context.
|
|
1234
|
+
*
|
|
1235
|
+
* The observed prompt size reflects the pre-reclaim conversation, so
|
|
1236
|
+
* leaving it in place would keep `shouldStop()` true forever and defeat
|
|
1237
|
+
* the reclaim. Resetting to the fail-open state means the guard stays
|
|
1238
|
+
* quiet until the next real usage report re-establishes the truth.
|
|
1239
|
+
*/
|
|
1240
|
+
resetAfterReclaim() {
|
|
1241
|
+
observedPromptTokens = 0;
|
|
1242
|
+
projectedGrowthTokens = 0;
|
|
1243
|
+
},
|
|
1232
1244
|
/** True when issuing another model call risks crossing the threshold. */
|
|
1233
1245
|
shouldStop() {
|
|
1234
1246
|
return (observedPromptTokens > 0 &&
|
|
@@ -51,51 +51,6 @@ export declare function stripAdditionalPropertiesDeep(schema: Record<string, unk
|
|
|
51
51
|
* @returns The region string to pass to the @google/genai client.
|
|
52
52
|
*/
|
|
53
53
|
export declare const resolveVertexLocation: (modelName: string | undefined, configuredLocation?: string) => string;
|
|
54
|
-
/**
|
|
55
|
-
* Google Vertex AI Provider v2 - BaseProvider Implementation
|
|
56
|
-
*
|
|
57
|
-
* Features:
|
|
58
|
-
* - Extends BaseProvider for shared functionality
|
|
59
|
-
* - Preserves existing Google Cloud authentication
|
|
60
|
-
* - Maintains Anthropic model support via dynamic imports
|
|
61
|
-
* - Fresh model creation for each request
|
|
62
|
-
* - Enhanced error handling with setup guidance
|
|
63
|
-
* - Tool registration and context management
|
|
64
|
-
*
|
|
65
|
-
* @important Tools + Schema Support (Fixed)
|
|
66
|
-
* Gemini models on Vertex AI now support combining function calling (tools) with
|
|
67
|
-
* structured output (JSON schema) simultaneously. The fix works by NOT setting
|
|
68
|
-
* `responseMimeType: "application/json"` when tools are present, which was
|
|
69
|
-
* causing the Google API error.
|
|
70
|
-
*
|
|
71
|
-
* The `responseSchema` is still set to guide the output structure, allowing
|
|
72
|
-
* tools to execute AND the final output to follow the schema format.
|
|
73
|
-
*
|
|
74
|
-
* @example Gemini models with tools + schemas
|
|
75
|
-
* ```typescript
|
|
76
|
-
* const provider = new GoogleVertexProvider("gemini-2.5-flash");
|
|
77
|
-
* const result = await provider.generate({
|
|
78
|
-
* input: { text: "Analyze data using tools" },
|
|
79
|
-
* schema: MySchema,
|
|
80
|
-
* output: { format: "json" },
|
|
81
|
-
* // No need for disableTools: true anymore!
|
|
82
|
-
* });
|
|
83
|
-
* ```
|
|
84
|
-
*
|
|
85
|
-
* @example Claude models (always supported both)
|
|
86
|
-
* ```typescript
|
|
87
|
-
* const provider = new GoogleVertexProvider("claude-3-5-sonnet-20241022");
|
|
88
|
-
* const result = await provider.generate({
|
|
89
|
-
* input: { text: "Analyze data" },
|
|
90
|
-
* schema: MySchema,
|
|
91
|
-
* output: { format: "json" }
|
|
92
|
-
* });
|
|
93
|
-
* ```
|
|
94
|
-
*
|
|
95
|
-
* @note "Too many states for serving" errors can still occur with very complex schemas + tools.
|
|
96
|
-
* Solution: Simplify schema or reduce number of tools if this occurs.
|
|
97
|
-
* @see https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models
|
|
98
|
-
*/
|
|
99
54
|
export declare class GoogleVertexProvider extends BaseProvider {
|
|
100
55
|
private projectId;
|
|
101
56
|
private location;
|