@juspay/neurolink 9.21.0 → 9.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/adapters/providerImageAdapter.js +32 -3
- package/dist/context/stages/slidingWindowTruncator.js +5 -6
- package/dist/lib/adapters/providerImageAdapter.js +32 -3
- package/dist/lib/context/stages/slidingWindowTruncator.js +5 -6
- package/dist/lib/providers/googleAiStudio.js +13 -4
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -1
- package/dist/lib/providers/googleNativeGemini3.js +3 -1
- package/dist/lib/providers/googleVertex.js +24 -17
- package/dist/providers/googleAiStudio.js +13 -4
- package/dist/providers/googleNativeGemini3.d.ts +4 -1
- package/dist/providers/googleNativeGemini3.js +3 -1
- package/dist/providers/googleVertex.js +24 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [9.22.1](https://github.com/juspay/neurolink/compare/v9.22.0...v9.22.1) (2026-03-12)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **(vision):** allow unknown models for proxy providers in vision check ([b2c5b4e](https://github.com/juspay/neurolink/commit/b2c5b4edebd43545dee8ccb31cb5253302602936))
|
|
6
|
+
|
|
7
|
+
## [9.22.0](https://github.com/juspay/neurolink/compare/v9.21.0...v9.22.0) (2026-03-12)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **(landing):** comprehensive mobile redesign across all 13 landing page components ([405e3e5](https://github.com/juspay/neurolink/commit/405e3e5eb8672b50ee1fc319088fb8c2b4fb78a0))
|
|
12
|
+
|
|
1
13
|
## [9.21.0](https://github.com/juspay/neurolink/compare/v9.20.0...v9.21.0) (2026-03-09)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -39,6 +39,24 @@ const IMAGE_LIMITS = {
|
|
|
39
39
|
bedrock: 20, // Same as Anthropic for Claude models on Bedrock
|
|
40
40
|
openrouter: 10, // Conservative limit, routes to various underlying providers
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Proxy providers that route to arbitrary underlying models.
|
|
44
|
+
* Vision capability cannot be statically determined for these — pass requests
|
|
45
|
+
* through and let the underlying provider surface errors if needed.
|
|
46
|
+
*/
|
|
47
|
+
const PROXY_PROVIDERS = new Set(["litellm", "openrouter"]);
|
|
48
|
+
/**
|
|
49
|
+
* Normalize provider name/alias to its canonical form for vision checks.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeVisionProvider(provider) {
|
|
52
|
+
const lower = provider.toLowerCase();
|
|
53
|
+
switch (lower) {
|
|
54
|
+
case "or":
|
|
55
|
+
return "openrouter";
|
|
56
|
+
default:
|
|
57
|
+
return lower;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
/**
|
|
43
61
|
* Vision capability definitions for each provider
|
|
44
62
|
*/
|
|
@@ -645,13 +663,18 @@ export class ProviderImageAdapter {
|
|
|
645
663
|
* Validate that provider and model support vision
|
|
646
664
|
*/
|
|
647
665
|
static validateVisionSupport(provider, model) {
|
|
648
|
-
const normalizedProvider = provider
|
|
666
|
+
const normalizedProvider = normalizeVisionProvider(provider);
|
|
649
667
|
const supportedModels = VISION_CAPABILITIES[normalizedProvider];
|
|
650
668
|
if (!supportedModels) {
|
|
651
669
|
throw new Error(`Provider ${provider} does not support vision processing. ` +
|
|
652
670
|
`Supported providers: ${Object.keys(VISION_CAPABILITIES).join(", ")}`);
|
|
653
671
|
}
|
|
654
672
|
const isSupported = supportedModels.some((supportedModel) => model.toLowerCase().includes(supportedModel.toLowerCase()));
|
|
673
|
+
// Proxy providers route to arbitrary underlying models — skip the allowlist
|
|
674
|
+
// check for unknown models and let the underlying provider error if needed.
|
|
675
|
+
if (!isSupported && PROXY_PROVIDERS.has(normalizedProvider)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
655
678
|
if (!isSupported) {
|
|
656
679
|
throw new Error(`Provider ${provider} with model ${model} does not support vision processing. ` +
|
|
657
680
|
`Supported models for ${provider}: ${supportedModels.join(", ")}`);
|
|
@@ -692,7 +715,7 @@ export class ProviderImageAdapter {
|
|
|
692
715
|
*/
|
|
693
716
|
static supportsVision(provider, model) {
|
|
694
717
|
try {
|
|
695
|
-
const normalizedProvider = provider
|
|
718
|
+
const normalizedProvider = normalizeVisionProvider(provider);
|
|
696
719
|
const supportedModels = VISION_CAPABILITIES[normalizedProvider];
|
|
697
720
|
if (!supportedModels) {
|
|
698
721
|
return false;
|
|
@@ -700,7 +723,13 @@ export class ProviderImageAdapter {
|
|
|
700
723
|
if (!model) {
|
|
701
724
|
return true; // Provider supports vision, but need to check specific model
|
|
702
725
|
}
|
|
703
|
-
|
|
726
|
+
const modelMatched = supportedModels.some((supportedModel) => model.toLowerCase().includes(supportedModel.toLowerCase()));
|
|
727
|
+
// Proxy providers route to arbitrary underlying models — pass through if
|
|
728
|
+
// the model isn't in the known allowlist.
|
|
729
|
+
if (!modelMatched && PROXY_PROVIDERS.has(normalizedProvider)) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
return modelMatched;
|
|
704
733
|
}
|
|
705
734
|
catch {
|
|
706
735
|
return false;
|
|
@@ -20,7 +20,6 @@ function validateRoleAlternation(messages) {
|
|
|
20
20
|
if (messages[i].role === messages[i - 1].role &&
|
|
21
21
|
messages[i].role !== "system") {
|
|
22
22
|
logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
|
|
23
|
-
break;
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
}
|
|
@@ -131,13 +130,13 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
131
130
|
break;
|
|
132
131
|
}
|
|
133
132
|
const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
|
|
134
|
-
// Insert a
|
|
135
|
-
//
|
|
133
|
+
// Insert a truncation marker with machine-readable metadata so
|
|
134
|
+
// effectiveHistory.ts can detect it via isTruncationMarker /
|
|
136
135
|
// truncationId and removeTruncationTags can rewind it.
|
|
137
136
|
const truncId = randomUUID();
|
|
138
137
|
const marker = {
|
|
139
138
|
id: `truncation-marker-${truncId}`,
|
|
140
|
-
role: "
|
|
139
|
+
role: "user",
|
|
141
140
|
content: TRUNCATION_MARKER_CONTENT,
|
|
142
141
|
isTruncationMarker: true,
|
|
143
142
|
truncationId: truncId,
|
|
@@ -170,11 +169,11 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
170
169
|
const evenMaxRemove = maxRemove - (maxRemove % 2);
|
|
171
170
|
if (evenMaxRemove > 0) {
|
|
172
171
|
const keptMessages = remainingMessages.slice(evenMaxRemove);
|
|
173
|
-
// Insert a
|
|
172
|
+
// Insert a truncation marker (see iterative block above)
|
|
174
173
|
const fallbackTruncId = randomUUID();
|
|
175
174
|
const fallbackMarker = {
|
|
176
175
|
id: `truncation-marker-${fallbackTruncId}`,
|
|
177
|
-
role: "
|
|
176
|
+
role: "user",
|
|
178
177
|
content: TRUNCATION_MARKER_CONTENT,
|
|
179
178
|
isTruncationMarker: true,
|
|
180
179
|
truncationId: fallbackTruncId,
|
|
@@ -39,6 +39,24 @@ const IMAGE_LIMITS = {
|
|
|
39
39
|
bedrock: 20, // Same as Anthropic for Claude models on Bedrock
|
|
40
40
|
openrouter: 10, // Conservative limit, routes to various underlying providers
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Proxy providers that route to arbitrary underlying models.
|
|
44
|
+
* Vision capability cannot be statically determined for these — pass requests
|
|
45
|
+
* through and let the underlying provider surface errors if needed.
|
|
46
|
+
*/
|
|
47
|
+
const PROXY_PROVIDERS = new Set(["litellm", "openrouter"]);
|
|
48
|
+
/**
|
|
49
|
+
* Normalize provider name/alias to its canonical form for vision checks.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeVisionProvider(provider) {
|
|
52
|
+
const lower = provider.toLowerCase();
|
|
53
|
+
switch (lower) {
|
|
54
|
+
case "or":
|
|
55
|
+
return "openrouter";
|
|
56
|
+
default:
|
|
57
|
+
return lower;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
/**
|
|
43
61
|
* Vision capability definitions for each provider
|
|
44
62
|
*/
|
|
@@ -645,13 +663,18 @@ export class ProviderImageAdapter {
|
|
|
645
663
|
* Validate that provider and model support vision
|
|
646
664
|
*/
|
|
647
665
|
static validateVisionSupport(provider, model) {
|
|
648
|
-
const normalizedProvider = provider
|
|
666
|
+
const normalizedProvider = normalizeVisionProvider(provider);
|
|
649
667
|
const supportedModels = VISION_CAPABILITIES[normalizedProvider];
|
|
650
668
|
if (!supportedModels) {
|
|
651
669
|
throw new Error(`Provider ${provider} does not support vision processing. ` +
|
|
652
670
|
`Supported providers: ${Object.keys(VISION_CAPABILITIES).join(", ")}`);
|
|
653
671
|
}
|
|
654
672
|
const isSupported = supportedModels.some((supportedModel) => model.toLowerCase().includes(supportedModel.toLowerCase()));
|
|
673
|
+
// Proxy providers route to arbitrary underlying models — skip the allowlist
|
|
674
|
+
// check for unknown models and let the underlying provider error if needed.
|
|
675
|
+
if (!isSupported && PROXY_PROVIDERS.has(normalizedProvider)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
655
678
|
if (!isSupported) {
|
|
656
679
|
throw new Error(`Provider ${provider} with model ${model} does not support vision processing. ` +
|
|
657
680
|
`Supported models for ${provider}: ${supportedModels.join(", ")}`);
|
|
@@ -692,7 +715,7 @@ export class ProviderImageAdapter {
|
|
|
692
715
|
*/
|
|
693
716
|
static supportsVision(provider, model) {
|
|
694
717
|
try {
|
|
695
|
-
const normalizedProvider = provider
|
|
718
|
+
const normalizedProvider = normalizeVisionProvider(provider);
|
|
696
719
|
const supportedModels = VISION_CAPABILITIES[normalizedProvider];
|
|
697
720
|
if (!supportedModels) {
|
|
698
721
|
return false;
|
|
@@ -700,7 +723,13 @@ export class ProviderImageAdapter {
|
|
|
700
723
|
if (!model) {
|
|
701
724
|
return true; // Provider supports vision, but need to check specific model
|
|
702
725
|
}
|
|
703
|
-
|
|
726
|
+
const modelMatched = supportedModels.some((supportedModel) => model.toLowerCase().includes(supportedModel.toLowerCase()));
|
|
727
|
+
// Proxy providers route to arbitrary underlying models — pass through if
|
|
728
|
+
// the model isn't in the known allowlist.
|
|
729
|
+
if (!modelMatched && PROXY_PROVIDERS.has(normalizedProvider)) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
return modelMatched;
|
|
704
733
|
}
|
|
705
734
|
catch {
|
|
706
735
|
return false;
|
|
@@ -20,7 +20,6 @@ function validateRoleAlternation(messages) {
|
|
|
20
20
|
if (messages[i].role === messages[i - 1].role &&
|
|
21
21
|
messages[i].role !== "system") {
|
|
22
22
|
logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
|
|
23
|
-
break;
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
}
|
|
@@ -131,13 +130,13 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
131
130
|
break;
|
|
132
131
|
}
|
|
133
132
|
const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
|
|
134
|
-
// Insert a
|
|
135
|
-
//
|
|
133
|
+
// Insert a truncation marker with machine-readable metadata so
|
|
134
|
+
// effectiveHistory.ts can detect it via isTruncationMarker /
|
|
136
135
|
// truncationId and removeTruncationTags can rewind it.
|
|
137
136
|
const truncId = randomUUID();
|
|
138
137
|
const marker = {
|
|
139
138
|
id: `truncation-marker-${truncId}`,
|
|
140
|
-
role: "
|
|
139
|
+
role: "user",
|
|
141
140
|
content: TRUNCATION_MARKER_CONTENT,
|
|
142
141
|
isTruncationMarker: true,
|
|
143
142
|
truncationId: truncId,
|
|
@@ -170,11 +169,11 @@ export function truncateWithSlidingWindow(messages, config) {
|
|
|
170
169
|
const evenMaxRemove = maxRemove - (maxRemove % 2);
|
|
171
170
|
if (evenMaxRemove > 0) {
|
|
172
171
|
const keptMessages = remainingMessages.slice(evenMaxRemove);
|
|
173
|
-
// Insert a
|
|
172
|
+
// Insert a truncation marker (see iterative block above)
|
|
174
173
|
const fallbackTruncId = randomUUID();
|
|
175
174
|
const fallbackMarker = {
|
|
176
175
|
id: `truncation-marker-${fallbackTruncId}`,
|
|
177
|
-
role: "
|
|
176
|
+
role: "user",
|
|
178
177
|
content: TRUNCATION_MARKER_CONTENT,
|
|
179
178
|
isTruncationMarker: true,
|
|
180
179
|
truncationId: fallbackTruncId,
|
|
@@ -457,9 +457,18 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
457
457
|
? { ...baseTools, ...(options.tools || {}) }
|
|
458
458
|
: {};
|
|
459
459
|
// Sanitize tool schemas for Gemini proto compatibility (converts anyOf/oneOf unions to string)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
let tools;
|
|
461
|
+
if (Object.keys(rawTools).length > 0) {
|
|
462
|
+
const sanitized = sanitizeToolsForGemini(rawTools);
|
|
463
|
+
if (sanitized.dropped.length > 0) {
|
|
464
|
+
logger.warn(`[GoogleAIStudio] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
|
|
465
|
+
}
|
|
466
|
+
tools =
|
|
467
|
+
Object.keys(sanitized.tools).length > 0 ? sanitized.tools : undefined;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
tools = undefined;
|
|
471
|
+
}
|
|
463
472
|
// Build message array from options with multimodal support
|
|
464
473
|
// Using protected helper from BaseProvider to eliminate code duplication
|
|
465
474
|
const messages = await this.buildMessagesForStream(options);
|
|
@@ -470,7 +479,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
470
479
|
maxTokens: options.maxTokens, // No default limit - unlimited unless specified
|
|
471
480
|
tools,
|
|
472
481
|
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
473
|
-
toolChoice: shouldUseTools ? "auto" : "none",
|
|
482
|
+
toolChoice: shouldUseTools && tools ? "auto" : "none",
|
|
474
483
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
475
484
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
476
485
|
// Gemini 3: use thinkingLevel via providerOptions
|
|
@@ -66,7 +66,10 @@ export declare function sanitizeSchemaForGemini(schema: Record<string, unknown>)
|
|
|
66
66
|
* This function pre-converts each tool's Zod parameters to sanitized JSON Schema
|
|
67
67
|
* and re-wraps with the Vercel AI SDK's jsonSchema() helper.
|
|
68
68
|
*/
|
|
69
|
-
export declare function sanitizeToolsForGemini(tools: Record<string, Tool>):
|
|
69
|
+
export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): {
|
|
70
|
+
tools: Record<string, Tool>;
|
|
71
|
+
dropped: string[];
|
|
72
|
+
};
|
|
70
73
|
/**
|
|
71
74
|
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
|
|
72
75
|
*
|
|
@@ -114,6 +114,7 @@ export function sanitizeSchemaForGemini(schema) {
|
|
|
114
114
|
*/
|
|
115
115
|
export function sanitizeToolsForGemini(tools) {
|
|
116
116
|
const sanitized = {};
|
|
117
|
+
const dropped = [];
|
|
117
118
|
for (const [name, tool] of Object.entries(tools)) {
|
|
118
119
|
try {
|
|
119
120
|
const params = tool.parameters;
|
|
@@ -156,10 +157,11 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
156
157
|
catch (error) {
|
|
157
158
|
logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
159
|
// Don't fall back to the original tool — an incompatible schema would fail the Gemini request
|
|
160
|
+
dropped.push(name);
|
|
159
161
|
continue;
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
|
-
return sanitized;
|
|
164
|
+
return { tools: sanitized, dropped };
|
|
163
165
|
}
|
|
164
166
|
/**
|
|
165
167
|
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
|
|
@@ -777,14 +777,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
777
777
|
async executeStream(options, analysisSchema) {
|
|
778
778
|
// Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
|
|
779
779
|
const gemini3CheckModelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
|
|
780
|
+
// Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
|
|
781
|
+
// Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
|
|
782
|
+
const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
|
|
780
783
|
// Check for tools from options AND from SDK (MCP tools)
|
|
781
784
|
// Need to check early if we should route to native SDK
|
|
782
|
-
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
|
|
785
|
+
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
|
|
783
786
|
const optionTools = options.tools || {};
|
|
784
787
|
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
785
788
|
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
786
789
|
const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
|
|
787
|
-
if (isGemini3Model(gemini3CheckModelName) && hasTools
|
|
790
|
+
if (isGemini3Model(gemini3CheckModelName) && hasTools) {
|
|
788
791
|
// Process CSV files before routing to native SDK (bypasses normal message builder)
|
|
789
792
|
const processedOptions = await this.processCSVFilesForNativeSDK(options);
|
|
790
793
|
// Merge SDK tools into options for native SDK path
|
|
@@ -792,16 +795,6 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
792
795
|
...processedOptions,
|
|
793
796
|
tools: { ...sdkTools, ...optionTools },
|
|
794
797
|
};
|
|
795
|
-
// Gemini cannot use tools and JSON schema simultaneously
|
|
796
|
-
const wantsStructuredOutput = analysisSchema ||
|
|
797
|
-
processedOptions.output?.format === "json" ||
|
|
798
|
-
processedOptions.schema;
|
|
799
|
-
if (wantsStructuredOutput) {
|
|
800
|
-
mergedOptions.tools = {};
|
|
801
|
-
mergedOptions.toolChoice = undefined;
|
|
802
|
-
mergedOptions.maxSteps = undefined;
|
|
803
|
-
logger.warn("[GoogleVertex] Structured output active — disabling tools for Gemini 3 (Gemini limitation).");
|
|
804
|
-
}
|
|
805
798
|
logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
|
|
806
799
|
model: gemini3CheckModelName,
|
|
807
800
|
optionToolCount: Object.keys(optionTools).length,
|
|
@@ -831,15 +824,28 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
831
824
|
: {};
|
|
832
825
|
// Only sanitize for Gemini models (not Anthropic/Claude models routed through Vertex)
|
|
833
826
|
const isAnthropic = isAnthropicModel(gemini3CheckModelName);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
827
|
+
let tools;
|
|
828
|
+
if (Object.keys(rawTools).length > 0 && !isAnthropic) {
|
|
829
|
+
const sanitized = sanitizeToolsForGemini(rawTools);
|
|
830
|
+
if (sanitized.dropped.length > 0) {
|
|
831
|
+
logger.warn(`[GoogleVertex] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
|
|
832
|
+
}
|
|
833
|
+
tools =
|
|
834
|
+
Object.keys(sanitized.tools).length > 0 ? sanitized.tools : undefined;
|
|
835
|
+
}
|
|
836
|
+
else if (isAnthropic && Object.keys(rawTools).length > 0) {
|
|
837
|
+
// Anthropic models don't need Gemini sanitization — pass tools through
|
|
838
|
+
tools = rawTools;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
tools = undefined;
|
|
842
|
+
}
|
|
837
843
|
logger.debug(`${functionTag}: Tools for streaming`, {
|
|
838
844
|
shouldUseTools,
|
|
839
845
|
baseToolCount: Object.keys(baseStreamTools).length,
|
|
840
846
|
externalToolCount: Object.keys(options.tools || {}).length,
|
|
841
|
-
toolCount: Object.keys(tools).length,
|
|
842
|
-
toolNames: Object.keys(tools),
|
|
847
|
+
toolCount: Object.keys(tools ?? {}).length,
|
|
848
|
+
toolNames: Object.keys(tools ?? {}),
|
|
843
849
|
});
|
|
844
850
|
// Model-specific maxTokens handling
|
|
845
851
|
const modelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
|
|
@@ -857,6 +863,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
857
863
|
...(maxTokens && { maxTokens }),
|
|
858
864
|
maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
|
|
859
865
|
...(shouldUseTools &&
|
|
866
|
+
tools &&
|
|
860
867
|
Object.keys(tools).length > 0 && {
|
|
861
868
|
tools,
|
|
862
869
|
toolChoice: "auto",
|
|
@@ -457,9 +457,18 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
457
457
|
? { ...baseTools, ...(options.tools || {}) }
|
|
458
458
|
: {};
|
|
459
459
|
// Sanitize tool schemas for Gemini proto compatibility (converts anyOf/oneOf unions to string)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
let tools;
|
|
461
|
+
if (Object.keys(rawTools).length > 0) {
|
|
462
|
+
const sanitized = sanitizeToolsForGemini(rawTools);
|
|
463
|
+
if (sanitized.dropped.length > 0) {
|
|
464
|
+
logger.warn(`[GoogleAIStudio] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
|
|
465
|
+
}
|
|
466
|
+
tools =
|
|
467
|
+
Object.keys(sanitized.tools).length > 0 ? sanitized.tools : undefined;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
tools = undefined;
|
|
471
|
+
}
|
|
463
472
|
// Build message array from options with multimodal support
|
|
464
473
|
// Using protected helper from BaseProvider to eliminate code duplication
|
|
465
474
|
const messages = await this.buildMessagesForStream(options);
|
|
@@ -470,7 +479,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
470
479
|
maxTokens: options.maxTokens, // No default limit - unlimited unless specified
|
|
471
480
|
tools,
|
|
472
481
|
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
473
|
-
toolChoice: shouldUseTools ? "auto" : "none",
|
|
482
|
+
toolChoice: shouldUseTools && tools ? "auto" : "none",
|
|
474
483
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
475
484
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
476
485
|
// Gemini 3: use thinkingLevel via providerOptions
|
|
@@ -66,7 +66,10 @@ export declare function sanitizeSchemaForGemini(schema: Record<string, unknown>)
|
|
|
66
66
|
* This function pre-converts each tool's Zod parameters to sanitized JSON Schema
|
|
67
67
|
* and re-wraps with the Vercel AI SDK's jsonSchema() helper.
|
|
68
68
|
*/
|
|
69
|
-
export declare function sanitizeToolsForGemini(tools: Record<string, Tool>):
|
|
69
|
+
export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): {
|
|
70
|
+
tools: Record<string, Tool>;
|
|
71
|
+
dropped: string[];
|
|
72
|
+
};
|
|
70
73
|
/**
|
|
71
74
|
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
|
|
72
75
|
*
|
|
@@ -114,6 +114,7 @@ export function sanitizeSchemaForGemini(schema) {
|
|
|
114
114
|
*/
|
|
115
115
|
export function sanitizeToolsForGemini(tools) {
|
|
116
116
|
const sanitized = {};
|
|
117
|
+
const dropped = [];
|
|
117
118
|
for (const [name, tool] of Object.entries(tools)) {
|
|
118
119
|
try {
|
|
119
120
|
const params = tool.parameters;
|
|
@@ -156,10 +157,11 @@ export function sanitizeToolsForGemini(tools) {
|
|
|
156
157
|
catch (error) {
|
|
157
158
|
logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
159
|
// Don't fall back to the original tool — an incompatible schema would fail the Gemini request
|
|
160
|
+
dropped.push(name);
|
|
159
161
|
continue;
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
|
-
return sanitized;
|
|
164
|
+
return { tools: sanitized, dropped };
|
|
163
165
|
}
|
|
164
166
|
/**
|
|
165
167
|
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
|
|
@@ -777,14 +777,17 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
777
777
|
async executeStream(options, analysisSchema) {
|
|
778
778
|
// Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
|
|
779
779
|
const gemini3CheckModelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
|
|
780
|
+
// Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
|
|
781
|
+
// Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
|
|
782
|
+
const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
|
|
780
783
|
// Check for tools from options AND from SDK (MCP tools)
|
|
781
784
|
// Need to check early if we should route to native SDK
|
|
782
|
-
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
|
|
785
|
+
const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
|
|
783
786
|
const optionTools = options.tools || {};
|
|
784
787
|
const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
|
|
785
788
|
const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
|
|
786
789
|
const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
|
|
787
|
-
if (isGemini3Model(gemini3CheckModelName) && hasTools
|
|
790
|
+
if (isGemini3Model(gemini3CheckModelName) && hasTools) {
|
|
788
791
|
// Process CSV files before routing to native SDK (bypasses normal message builder)
|
|
789
792
|
const processedOptions = await this.processCSVFilesForNativeSDK(options);
|
|
790
793
|
// Merge SDK tools into options for native SDK path
|
|
@@ -792,16 +795,6 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
792
795
|
...processedOptions,
|
|
793
796
|
tools: { ...sdkTools, ...optionTools },
|
|
794
797
|
};
|
|
795
|
-
// Gemini cannot use tools and JSON schema simultaneously
|
|
796
|
-
const wantsStructuredOutput = analysisSchema ||
|
|
797
|
-
processedOptions.output?.format === "json" ||
|
|
798
|
-
processedOptions.schema;
|
|
799
|
-
if (wantsStructuredOutput) {
|
|
800
|
-
mergedOptions.tools = {};
|
|
801
|
-
mergedOptions.toolChoice = undefined;
|
|
802
|
-
mergedOptions.maxSteps = undefined;
|
|
803
|
-
logger.warn("[GoogleVertex] Structured output active — disabling tools for Gemini 3 (Gemini limitation).");
|
|
804
|
-
}
|
|
805
798
|
logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
|
|
806
799
|
model: gemini3CheckModelName,
|
|
807
800
|
optionToolCount: Object.keys(optionTools).length,
|
|
@@ -831,15 +824,28 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
831
824
|
: {};
|
|
832
825
|
// Only sanitize for Gemini models (not Anthropic/Claude models routed through Vertex)
|
|
833
826
|
const isAnthropic = isAnthropicModel(gemini3CheckModelName);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
827
|
+
let tools;
|
|
828
|
+
if (Object.keys(rawTools).length > 0 && !isAnthropic) {
|
|
829
|
+
const sanitized = sanitizeToolsForGemini(rawTools);
|
|
830
|
+
if (sanitized.dropped.length > 0) {
|
|
831
|
+
logger.warn(`[GoogleVertex] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
|
|
832
|
+
}
|
|
833
|
+
tools =
|
|
834
|
+
Object.keys(sanitized.tools).length > 0 ? sanitized.tools : undefined;
|
|
835
|
+
}
|
|
836
|
+
else if (isAnthropic && Object.keys(rawTools).length > 0) {
|
|
837
|
+
// Anthropic models don't need Gemini sanitization — pass tools through
|
|
838
|
+
tools = rawTools;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
tools = undefined;
|
|
842
|
+
}
|
|
837
843
|
logger.debug(`${functionTag}: Tools for streaming`, {
|
|
838
844
|
shouldUseTools,
|
|
839
845
|
baseToolCount: Object.keys(baseStreamTools).length,
|
|
840
846
|
externalToolCount: Object.keys(options.tools || {}).length,
|
|
841
|
-
toolCount: Object.keys(tools).length,
|
|
842
|
-
toolNames: Object.keys(tools),
|
|
847
|
+
toolCount: Object.keys(tools ?? {}).length,
|
|
848
|
+
toolNames: Object.keys(tools ?? {}),
|
|
843
849
|
});
|
|
844
850
|
// Model-specific maxTokens handling
|
|
845
851
|
const modelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
|
|
@@ -857,6 +863,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
857
863
|
...(maxTokens && { maxTokens }),
|
|
858
864
|
maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
|
|
859
865
|
...(shouldUseTools &&
|
|
866
|
+
tools &&
|
|
860
867
|
Object.keys(tools).length > 0 && {
|
|
861
868
|
tools,
|
|
862
869
|
toolChoice: "auto",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.22.1",
|
|
4
4
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Juspay Technologies",
|