@juspay/neurolink 9.60.1 → 9.61.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/README.md +1 -0
- package/dist/browser/neurolink.min.js +343 -343
- package/dist/core/baseProvider.js +1 -1
- package/dist/core/constants.js +14 -3
- package/dist/core/modules/GenerationHandler.js +1 -1
- package/dist/core/redisConversationMemoryManager.d.ts +1 -0
- package/dist/core/redisConversationMemoryManager.js +11 -7
- package/dist/lib/core/baseProvider.js +1 -1
- package/dist/lib/core/constants.js +14 -3
- package/dist/lib/core/modules/GenerationHandler.js +1 -1
- package/dist/lib/core/redisConversationMemoryManager.d.ts +1 -0
- package/dist/lib/core/redisConversationMemoryManager.js +11 -7
- package/dist/lib/mcp/auth/oauthClientProvider.js +12 -7
- package/dist/lib/types/tools.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.d.ts +12 -4
- package/dist/lib/utils/schemaConversion.js +208 -27
- package/dist/lib/utils/tokenLimits.js +43 -1
- package/dist/mcp/auth/oauthClientProvider.js +12 -7
- package/dist/types/tools.d.ts +1 -0
- package/dist/utils/schemaConversion.d.ts +12 -4
- package/dist/utils/schemaConversion.js +208 -27
- package/dist/utils/tokenLimits.js +43 -1
- package/package.json +1 -1
|
@@ -2,12 +2,54 @@
|
|
|
2
2
|
* Provider-specific token limit utilities
|
|
3
3
|
* Provides safe maxTokens values based on provider and model capabilities
|
|
4
4
|
*/
|
|
5
|
-
import { PROVIDER_MAX_TOKENS } from "../core/constants.js";
|
|
5
|
+
import { PROVIDER_MAX_TOKENS, IMAGE_GENERATION_MODELS, } from "../core/constants.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
|
+
// Gemini 3 models and Gemini 2.5 image models have a hard limit of 32768 output tokens
|
|
8
|
+
const GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS = 32768;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a model has the restricted 32768 output token limit
|
|
11
|
+
* This applies to:
|
|
12
|
+
* - All Gemini 3 models (gemini-3-flash, gemini-3-pro, etc.)
|
|
13
|
+
* - All Gemini 2.5 image generation models (gemini-2.5-flash-image)
|
|
14
|
+
*/
|
|
15
|
+
function hasRestrictedOutputLimit(model) {
|
|
16
|
+
if (!model) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Check for Gemini 3 models
|
|
20
|
+
if (model.includes("gemini-3")) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
// Check for image generation models (includes gemini-2.5-flash-image)
|
|
24
|
+
if (IMAGE_GENERATION_MODELS.some((m) => model.includes(m))) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
7
29
|
/**
|
|
8
30
|
* Get the safe maximum tokens for a provider and model
|
|
9
31
|
*/
|
|
10
32
|
export function getSafeMaxTokens(provider, model, requestedMaxTokens) {
|
|
33
|
+
// CRITICAL: Gemini 3 models AND image generation models have a hard limit of 32768 output tokens
|
|
34
|
+
// This check must happen FIRST, before any other logic, because these models
|
|
35
|
+
// will reject requests with maxOutputTokens > 32768
|
|
36
|
+
const isRestrictedModel = hasRestrictedOutputLimit(model);
|
|
37
|
+
if (isRestrictedModel) {
|
|
38
|
+
// Explicit undefined/null check so a caller-supplied 0 is preserved
|
|
39
|
+
// (truthy checks would treat 0 as "unset" and silently fall back to the cap).
|
|
40
|
+
if (requestedMaxTokens !== undefined &&
|
|
41
|
+
requestedMaxTokens !== null &&
|
|
42
|
+
requestedMaxTokens > GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS) {
|
|
43
|
+
logger.warn(`Requested maxTokens ${requestedMaxTokens} exceeds ${model} limit of ${GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS}. Using ${GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS} instead.`);
|
|
44
|
+
return GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS;
|
|
45
|
+
}
|
|
46
|
+
// If no maxTokens specified, use the restricted limit as default
|
|
47
|
+
if (requestedMaxTokens === undefined || requestedMaxTokens === null) {
|
|
48
|
+
return GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS;
|
|
49
|
+
}
|
|
50
|
+
// Otherwise, use the requested value (it's within limits, including 0)
|
|
51
|
+
return requestedMaxTokens;
|
|
52
|
+
}
|
|
11
53
|
// Get provider-specific limits
|
|
12
54
|
const providerLimits = PROVIDER_MAX_TOKENS[provider];
|
|
13
55
|
if (!providerLimits) {
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
import { randomBytes, createHash } from "crypto";
|
|
6
6
|
import { InMemoryTokenStorage, isTokenExpired, calculateExpiresAt, } from "./tokenStorage.js";
|
|
7
7
|
import { logger } from "../../utils/logger.js";
|
|
8
|
+
import { withTimeout } from "../../utils/errorHandling.js";
|
|
9
|
+
/** Default timeout for OAuth token operations (30 seconds) */
|
|
10
|
+
const OAUTH_TOKEN_TIMEOUT_MS = 30000;
|
|
8
11
|
/**
|
|
9
12
|
* NeuroLink OAuth Provider for MCP HTTP Transport
|
|
10
13
|
* Handles OAuth 2.1 authentication flow with optional PKCE support
|
|
@@ -147,15 +150,15 @@ export class NeuroLinkOAuthProvider {
|
|
|
147
150
|
if (codeVerifier) {
|
|
148
151
|
body.set("code_verifier", codeVerifier);
|
|
149
152
|
}
|
|
150
|
-
// Request tokens
|
|
151
|
-
const response = await fetch(this.config.tokenUrl, {
|
|
153
|
+
// Request tokens with timeout protection
|
|
154
|
+
const response = await withTimeout(fetch(this.config.tokenUrl, {
|
|
152
155
|
method: "POST",
|
|
153
156
|
headers: {
|
|
154
157
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
155
158
|
Accept: "application/json",
|
|
156
159
|
},
|
|
157
160
|
body: body.toString(),
|
|
158
|
-
});
|
|
161
|
+
}), OAUTH_TOKEN_TIMEOUT_MS, new Error(`OAuth token exchange timed out after ${OAUTH_TOKEN_TIMEOUT_MS}ms`));
|
|
159
162
|
if (!response.ok) {
|
|
160
163
|
const errorText = await response.text();
|
|
161
164
|
throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
@@ -186,14 +189,15 @@ export class NeuroLinkOAuthProvider {
|
|
|
186
189
|
if (this.config.clientSecret) {
|
|
187
190
|
body.set("client_secret", this.config.clientSecret);
|
|
188
191
|
}
|
|
189
|
-
|
|
192
|
+
// Refresh tokens with timeout protection
|
|
193
|
+
const response = await withTimeout(fetch(this.config.tokenUrl, {
|
|
190
194
|
method: "POST",
|
|
191
195
|
headers: {
|
|
192
196
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
193
197
|
Accept: "application/json",
|
|
194
198
|
},
|
|
195
199
|
body: body.toString(),
|
|
196
|
-
});
|
|
200
|
+
}), OAUTH_TOKEN_TIMEOUT_MS, new Error(`OAuth token refresh timed out after ${OAUTH_TOKEN_TIMEOUT_MS}ms`));
|
|
197
201
|
if (!response.ok) {
|
|
198
202
|
const errorText = await response.text();
|
|
199
203
|
throw new Error(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
@@ -227,13 +231,14 @@ export class NeuroLinkOAuthProvider {
|
|
|
227
231
|
body.set("client_secret", this.config.clientSecret);
|
|
228
232
|
}
|
|
229
233
|
try {
|
|
230
|
-
|
|
234
|
+
// Revoke tokens with timeout protection
|
|
235
|
+
await withTimeout(fetch(revocationUrl, {
|
|
231
236
|
method: "POST",
|
|
232
237
|
headers: {
|
|
233
238
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
234
239
|
},
|
|
235
240
|
body: body.toString(),
|
|
236
|
-
});
|
|
241
|
+
}), OAUTH_TOKEN_TIMEOUT_MS, new Error(`OAuth token revocation timed out after ${OAUTH_TOKEN_TIMEOUT_MS}ms`));
|
|
237
242
|
}
|
|
238
243
|
catch (error) {
|
|
239
244
|
logger.warn(`[NeuroLinkOAuthProvider] Token revocation failed: ${error instanceof Error ? error.message : String(error)}`);
|
package/dist/types/tools.d.ts
CHANGED
|
@@ -8,16 +8,24 @@ import type { ZodUnknownSchema } from "../types/index.js";
|
|
|
8
8
|
* - Top-level $ref resolution
|
|
9
9
|
* - Nested $ref within properties, items, additionalProperties
|
|
10
10
|
* - $ref within allOf, anyOf, oneOf arrays
|
|
11
|
+
* - Deep $ref paths like "#/definitions/Foo/properties/bar"
|
|
11
12
|
* - Circular reference detection to prevent infinite loops
|
|
12
13
|
*/
|
|
13
|
-
export declare function inlineJsonSchema(schema: Record<string, unknown>, definitions?: Record<string, Record<string, unknown>>, visited?: Set<string>): Record<string, unknown>;
|
|
14
|
+
export declare function inlineJsonSchema(schema: Record<string, unknown>, definitions?: Record<string, Record<string, unknown>>, visited?: Set<string>, rootSchema?: Record<string, unknown>): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Recursively ensure all nested schemas have a type field.
|
|
17
|
+
* Google Vertex AI requires ALL schema objects (including nested properties) to have a type field.
|
|
18
|
+
* This function walks through the schema tree and adds type:"object" to any object-like schema
|
|
19
|
+
* that's missing its type field.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureNestedSchemaTypes(schema: Record<string, unknown>): Record<string, unknown>;
|
|
14
22
|
/**
|
|
15
23
|
* Convert Zod schema to JSON Schema format for provider APIs.
|
|
16
24
|
*
|
|
17
25
|
* Handles three input types:
|
|
18
|
-
* 1. Zod schemas (have `_def.typeName`)
|
|
19
|
-
* 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property)
|
|
20
|
-
* 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`)
|
|
26
|
+
* 1. Zod schemas (have `_def.typeName`) -- converted via zod-to-json-schema
|
|
27
|
+
* 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) -- extracted directly
|
|
28
|
+
* 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) -- returned as-is
|
|
21
29
|
*/
|
|
22
30
|
export declare function convertZodToJsonSchema(zodSchema: ZodUnknownSchema): object;
|
|
23
31
|
export declare function normalizeJsonSchemaObject(schema: Record<string, unknown> | undefined | null): Record<string, unknown>;
|
|
@@ -2,6 +2,53 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
2
2
|
import { jsonSchemaToZod } from "json-schema-to-zod";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { logger } from "./logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a deep JSON pointer path within a schema.
|
|
7
|
+
* Handles paths like "#/definitions/ToolParameters/properties/foo/properties/bar"
|
|
8
|
+
*
|
|
9
|
+
* Implements RFC 6901 token decoding so property names containing the literal
|
|
10
|
+
* characters "/" or "~" can still be resolved (their escaped forms are "~1"
|
|
11
|
+
* and "~0" respectively). Because "#/..." is the URI-fragment form of a JSON
|
|
12
|
+
* Pointer (RFC 6901 §6), each segment may also be percent-encoded; we decode
|
|
13
|
+
* that first.
|
|
14
|
+
*
|
|
15
|
+
* Order matters: percent-decode → "~1" → "/" → "~0" → "~". Reversing the
|
|
16
|
+
* tilde steps would let "~01" round-trip to "/" instead of the intended "~1".
|
|
17
|
+
*/
|
|
18
|
+
function resolveDeepRef(rootSchema, refPath) {
|
|
19
|
+
// Strip the leading "#/" then split + decode each segment per RFC 6901
|
|
20
|
+
const pathParts = refPath
|
|
21
|
+
.replace(/^#\//, "")
|
|
22
|
+
.split("/")
|
|
23
|
+
.map((seg) => safePercentDecode(seg).replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
24
|
+
let current = rootSchema;
|
|
25
|
+
for (const part of pathParts) {
|
|
26
|
+
if (current && typeof current === "object" && part in current) {
|
|
27
|
+
current = current[part];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (current && typeof current === "object") {
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Percent-decode a JSON Pointer segment defensively. Falls back to the raw
|
|
40
|
+
* segment if the input contains a malformed escape sequence (decodeURIComponent
|
|
41
|
+
* throws URIError on those) — better to attempt a literal match than fail the
|
|
42
|
+
* whole resolution.
|
|
43
|
+
*/
|
|
44
|
+
function safePercentDecode(segment) {
|
|
45
|
+
try {
|
|
46
|
+
return decodeURIComponent(segment);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return segment;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
5
52
|
/**
|
|
6
53
|
* Inline a JSON Schema by recursively resolving all $ref references.
|
|
7
54
|
* zodToJsonSchema with 'name' option produces schemas with $ref pointing to definitions.
|
|
@@ -11,29 +58,45 @@ import { logger } from "./logger.js";
|
|
|
11
58
|
* - Top-level $ref resolution
|
|
12
59
|
* - Nested $ref within properties, items, additionalProperties
|
|
13
60
|
* - $ref within allOf, anyOf, oneOf arrays
|
|
61
|
+
* - Deep $ref paths like "#/definitions/Foo/properties/bar"
|
|
14
62
|
* - Circular reference detection to prevent infinite loops
|
|
15
63
|
*/
|
|
16
|
-
export function inlineJsonSchema(schema, definitions, visited = new Set()) {
|
|
64
|
+
export function inlineJsonSchema(schema, definitions, visited = new Set(), rootSchema) {
|
|
17
65
|
// Use definitions from schema if not provided
|
|
18
66
|
const defs = definitions ||
|
|
19
67
|
schema.definitions;
|
|
68
|
+
// Keep track of the root schema for deep ref resolution
|
|
69
|
+
const root = rootSchema || schema;
|
|
20
70
|
// Handle $ref at current level
|
|
21
|
-
if (typeof schema.$ref === "string" &&
|
|
22
|
-
schema.$ref
|
|
23
|
-
const defName = schema.$ref.replace("#/definitions/", "");
|
|
71
|
+
if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/")) {
|
|
72
|
+
const refPath = schema.$ref;
|
|
24
73
|
// Prevent circular reference infinite loops
|
|
25
|
-
if (visited.has(
|
|
26
|
-
logger.debug(`[SCHEMA-INLINE] Circular reference detected for: ${
|
|
74
|
+
if (visited.has(refPath)) {
|
|
75
|
+
logger.debug(`[SCHEMA-INLINE] Circular reference detected for: ${refPath}`);
|
|
27
76
|
// Return a simple object placeholder for circular refs
|
|
28
77
|
return { type: "object" };
|
|
29
78
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
79
|
+
// Try simple definition lookup first (for #/definitions/SomeName)
|
|
80
|
+
if (refPath.startsWith("#/definitions/")) {
|
|
81
|
+
const defName = refPath.replace("#/definitions/", "");
|
|
82
|
+
// Check if it's a simple definition name (no slashes after definitions/)
|
|
83
|
+
if (!defName.includes("/") && defs && defs[defName]) {
|
|
84
|
+
visited.add(refPath);
|
|
85
|
+
const resolved = inlineJsonSchema({ ...defs[defName] }, defs, visited, root);
|
|
86
|
+
visited.delete(refPath);
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Try deep path resolution for complex paths like
|
|
91
|
+
// #/definitions/ToolParameters/properties/accountPerformance/properties/roas
|
|
92
|
+
const resolved = resolveDeepRef(root, refPath);
|
|
93
|
+
if (resolved) {
|
|
94
|
+
visited.add(refPath);
|
|
95
|
+
const inlined = inlineJsonSchema({ ...resolved }, defs, visited, root);
|
|
96
|
+
visited.delete(refPath);
|
|
97
|
+
return inlined;
|
|
36
98
|
}
|
|
99
|
+
logger.debug(`[SCHEMA-INLINE] Could not resolve $ref: ${refPath}`);
|
|
37
100
|
}
|
|
38
101
|
// Create result without $ref and definitions
|
|
39
102
|
const result = {};
|
|
@@ -47,7 +110,7 @@ export function inlineJsonSchema(schema, definitions, visited = new Set()) {
|
|
|
47
110
|
const properties = {};
|
|
48
111
|
for (const [propName, propSchema] of Object.entries(value)) {
|
|
49
112
|
if (propSchema && typeof propSchema === "object") {
|
|
50
|
-
properties[propName] = inlineJsonSchema(propSchema, defs, visited);
|
|
113
|
+
properties[propName] = inlineJsonSchema(propSchema, defs, visited, root);
|
|
51
114
|
}
|
|
52
115
|
else {
|
|
53
116
|
properties[propName] = propSchema;
|
|
@@ -59,32 +122,32 @@ export function inlineJsonSchema(schema, definitions, visited = new Set()) {
|
|
|
59
122
|
// Handle array items schema
|
|
60
123
|
if (Array.isArray(value)) {
|
|
61
124
|
result[key] = value.map((item) => item && typeof item === "object"
|
|
62
|
-
? inlineJsonSchema(item, defs, visited)
|
|
125
|
+
? inlineJsonSchema(item, defs, visited, root)
|
|
63
126
|
: item);
|
|
64
127
|
}
|
|
65
128
|
else {
|
|
66
|
-
result[key] = inlineJsonSchema(value, defs, visited);
|
|
129
|
+
result[key] = inlineJsonSchema(value, defs, visited, root);
|
|
67
130
|
}
|
|
68
131
|
}
|
|
69
132
|
else if (key === "additionalProperties" &&
|
|
70
133
|
value &&
|
|
71
134
|
typeof value === "object") {
|
|
72
|
-
result[key] = inlineJsonSchema(value, defs, visited);
|
|
135
|
+
result[key] = inlineJsonSchema(value, defs, visited, root);
|
|
73
136
|
}
|
|
74
137
|
else if ((key === "allOf" || key === "anyOf" || key === "oneOf") &&
|
|
75
138
|
Array.isArray(value)) {
|
|
76
139
|
// Handle composition schemas
|
|
77
140
|
result[key] = value.map((item) => item && typeof item === "object"
|
|
78
|
-
? inlineJsonSchema(item, defs, visited)
|
|
141
|
+
? inlineJsonSchema(item, defs, visited, root)
|
|
79
142
|
: item);
|
|
80
143
|
}
|
|
81
144
|
else if (key === "not" && value && typeof value === "object") {
|
|
82
|
-
result[key] = inlineJsonSchema(value, defs, visited);
|
|
145
|
+
result[key] = inlineJsonSchema(value, defs, visited, root);
|
|
83
146
|
}
|
|
84
147
|
else if ((key === "if" || key === "then" || key === "else") &&
|
|
85
148
|
value &&
|
|
86
149
|
typeof value === "object") {
|
|
87
|
-
result[key] = inlineJsonSchema(value, defs, visited);
|
|
150
|
+
result[key] = inlineJsonSchema(value, defs, visited, root);
|
|
88
151
|
}
|
|
89
152
|
else {
|
|
90
153
|
result[key] = value;
|
|
@@ -92,13 +155,129 @@ export function inlineJsonSchema(schema, definitions, visited = new Set()) {
|
|
|
92
155
|
}
|
|
93
156
|
return result;
|
|
94
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Recursively ensure all nested schemas have a type field.
|
|
160
|
+
* Google Vertex AI requires ALL schema objects (including nested properties) to have a type field.
|
|
161
|
+
* This function walks through the schema tree and adds type:"object" to any object-like schema
|
|
162
|
+
* that's missing its type field.
|
|
163
|
+
*/
|
|
164
|
+
export function ensureNestedSchemaTypes(schema) {
|
|
165
|
+
if (!schema || typeof schema !== "object") {
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
let result = { ...schema };
|
|
169
|
+
// CRITICAL FIX: Flatten single-item allOf for Google Vertex AI compatibility
|
|
170
|
+
// When we have { allOf: [{ type: "object", ... }], nullable: true }, flatten it to:
|
|
171
|
+
// { type: "object", ..., nullable: true }
|
|
172
|
+
if (result.allOf &&
|
|
173
|
+
Array.isArray(result.allOf) &&
|
|
174
|
+
result.allOf.length === 1 &&
|
|
175
|
+
result.allOf[0] &&
|
|
176
|
+
typeof result.allOf[0] === "object") {
|
|
177
|
+
const innerSchema = result.allOf[0];
|
|
178
|
+
// Only flatten if inner schema has meaningful content (type, properties, items, etc.)
|
|
179
|
+
if (innerSchema.type ||
|
|
180
|
+
innerSchema.properties ||
|
|
181
|
+
innerSchema.items ||
|
|
182
|
+
innerSchema.enum) {
|
|
183
|
+
logger.debug(`[SCHEMA-TYPE-FIX] Flattening single-item allOf with type: ${innerSchema.type}`);
|
|
184
|
+
// Merge: inner schema properties take precedence, except for wrapper's metadata
|
|
185
|
+
const { allOf: _ignored, ...wrapperProps } = result;
|
|
186
|
+
result = {
|
|
187
|
+
...innerSchema,
|
|
188
|
+
...wrapperProps, // Keep wrapper's nullable, description, etc.
|
|
189
|
+
};
|
|
190
|
+
// If inner schema had its own nullable/description, restore them
|
|
191
|
+
if (innerSchema.description && !wrapperProps.description) {
|
|
192
|
+
result.description = innerSchema.description;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Infer type from structure if missing
|
|
197
|
+
if (!result.type) {
|
|
198
|
+
// If it has properties, it's an object
|
|
199
|
+
if (result.properties) {
|
|
200
|
+
result.type = "object";
|
|
201
|
+
logger.debug(`[SCHEMA-TYPE-FIX] Added type:"object" to schema with properties`);
|
|
202
|
+
}
|
|
203
|
+
// If it has items, it's an array
|
|
204
|
+
else if (result.items) {
|
|
205
|
+
result.type = "array";
|
|
206
|
+
logger.debug(`[SCHEMA-TYPE-FIX] Added type:"array" to schema with items`);
|
|
207
|
+
}
|
|
208
|
+
// If it has enum, infer from enum values — but only when ALL elements
|
|
209
|
+
// share the same primitive type. A mixed enum like [1, "x"] would
|
|
210
|
+
// otherwise be silently narrowed to whatever the first element is.
|
|
211
|
+
else if (result.enum &&
|
|
212
|
+
Array.isArray(result.enum) &&
|
|
213
|
+
result.enum.length > 0) {
|
|
214
|
+
if (result.enum.every((v) => typeof v === "string")) {
|
|
215
|
+
result.type = "string";
|
|
216
|
+
logger.debug(`[SCHEMA-TYPE-FIX] Added type:"string" to schema with enum`);
|
|
217
|
+
}
|
|
218
|
+
else if (result.enum.every((v) => typeof v === "number")) {
|
|
219
|
+
result.type = "number";
|
|
220
|
+
logger.debug(`[SCHEMA-TYPE-FIX] Added type:"number" to schema with enum`);
|
|
221
|
+
}
|
|
222
|
+
// Mixed-type enum: leave result.type unset rather than narrow it.
|
|
223
|
+
}
|
|
224
|
+
// If it has allOf with typed schemas, infer from first item
|
|
225
|
+
else if (result.allOf &&
|
|
226
|
+
Array.isArray(result.allOf) &&
|
|
227
|
+
result.allOf.length > 0) {
|
|
228
|
+
const firstItem = result.allOf[0];
|
|
229
|
+
if (firstItem && firstItem.type) {
|
|
230
|
+
result.type = firstItem.type;
|
|
231
|
+
logger.debug(`[SCHEMA-TYPE-FIX] Inferred type from allOf: ${result.type}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Recursively process properties
|
|
236
|
+
if (result.properties && typeof result.properties === "object") {
|
|
237
|
+
const properties = {};
|
|
238
|
+
for (const [propName, propSchema] of Object.entries(result.properties)) {
|
|
239
|
+
if (propSchema && typeof propSchema === "object") {
|
|
240
|
+
properties[propName] = ensureNestedSchemaTypes(propSchema);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
properties[propName] = propSchema;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
result.properties = properties;
|
|
247
|
+
}
|
|
248
|
+
// Recursively process items (for arrays)
|
|
249
|
+
if (result.items && typeof result.items === "object") {
|
|
250
|
+
if (Array.isArray(result.items)) {
|
|
251
|
+
result.items = result.items.map((item) => item && typeof item === "object"
|
|
252
|
+
? ensureNestedSchemaTypes(item)
|
|
253
|
+
: item);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
result.items = ensureNestedSchemaTypes(result.items);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Recursively process additionalProperties
|
|
260
|
+
if (result.additionalProperties &&
|
|
261
|
+
typeof result.additionalProperties === "object") {
|
|
262
|
+
result.additionalProperties = ensureNestedSchemaTypes(result.additionalProperties);
|
|
263
|
+
}
|
|
264
|
+
// Recursively process allOf, anyOf, oneOf
|
|
265
|
+
for (const key of ["allOf", "anyOf", "oneOf"]) {
|
|
266
|
+
if (result[key] && Array.isArray(result[key])) {
|
|
267
|
+
result[key] = result[key].map((item) => item && typeof item === "object"
|
|
268
|
+
? ensureNestedSchemaTypes(item)
|
|
269
|
+
: item);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
95
274
|
/**
|
|
96
275
|
* Convert Zod schema to JSON Schema format for provider APIs.
|
|
97
276
|
*
|
|
98
277
|
* Handles three input types:
|
|
99
|
-
* 1. Zod schemas (have `_def.typeName`)
|
|
100
|
-
* 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property)
|
|
101
|
-
* 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`)
|
|
278
|
+
* 1. Zod schemas (have `_def.typeName`) -- converted via zod-to-json-schema
|
|
279
|
+
* 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) -- extracted directly
|
|
280
|
+
* 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) -- returned as-is
|
|
102
281
|
*/
|
|
103
282
|
export function convertZodToJsonSchema(zodSchema) {
|
|
104
283
|
const schema = zodSchema;
|
|
@@ -110,11 +289,11 @@ export function convertZodToJsonSchema(zodSchema) {
|
|
|
110
289
|
schema.jsonSchema !== null &&
|
|
111
290
|
typeof schema.jsonSchema === "object") {
|
|
112
291
|
const extracted = schema.jsonSchema;
|
|
113
|
-
return ensureTypeField(extracted);
|
|
292
|
+
return ensureNestedSchemaTypes(ensureTypeField(extracted));
|
|
114
293
|
}
|
|
115
294
|
// Plain JSON Schema object (from external MCP tools) — no Zod internals
|
|
116
295
|
if (!isZodSchema(schema)) {
|
|
117
|
-
return ensureTypeField(schema);
|
|
296
|
+
return ensureNestedSchemaTypes(ensureTypeField(schema));
|
|
118
297
|
}
|
|
119
298
|
// Actual Zod schema — convert via zod-to-json-schema
|
|
120
299
|
try {
|
|
@@ -123,13 +302,15 @@ export function convertZodToJsonSchema(zodSchema) {
|
|
|
123
302
|
const zodV3Schema = zodSchema;
|
|
124
303
|
const jsonSchema = zodToJsonSchema(zodV3Schema, {
|
|
125
304
|
name: "ToolParameters",
|
|
126
|
-
target: "
|
|
305
|
+
target: "openApi3", // Use OpenAPI 3.0 for nullable: true instead of anyOf with null (required for Vertex AI)
|
|
127
306
|
errorMessages: true,
|
|
128
307
|
});
|
|
129
308
|
// zodToJsonSchema with 'name' produces { $ref: "#/definitions/ToolParameters", definitions: {...} }
|
|
130
|
-
// Inline the $ref to produce a flat schema
|
|
309
|
+
// Inline the $ref to produce a flat schema, ensure the root has a type
|
|
310
|
+
// field, then walk the tree so nested objects/arrays/additionalProperties
|
|
311
|
+
// also pick up an inferred type (Vertex/Gemini require it everywhere).
|
|
131
312
|
const inlined = inlineJsonSchema(jsonSchema);
|
|
132
|
-
return ensureTypeField(inlined);
|
|
313
|
+
return ensureNestedSchemaTypes(ensureTypeField(inlined));
|
|
133
314
|
}
|
|
134
315
|
catch (error) {
|
|
135
316
|
logger.warn("Failed to convert Zod schema to JSON Schema", {
|
|
@@ -2,12 +2,54 @@
|
|
|
2
2
|
* Provider-specific token limit utilities
|
|
3
3
|
* Provides safe maxTokens values based on provider and model capabilities
|
|
4
4
|
*/
|
|
5
|
-
import { PROVIDER_MAX_TOKENS } from "../core/constants.js";
|
|
5
|
+
import { PROVIDER_MAX_TOKENS, IMAGE_GENERATION_MODELS, } from "../core/constants.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
|
+
// Gemini 3 models and Gemini 2.5 image models have a hard limit of 32768 output tokens
|
|
8
|
+
const GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS = 32768;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a model has the restricted 32768 output token limit
|
|
11
|
+
* This applies to:
|
|
12
|
+
* - All Gemini 3 models (gemini-3-flash, gemini-3-pro, etc.)
|
|
13
|
+
* - All Gemini 2.5 image generation models (gemini-2.5-flash-image)
|
|
14
|
+
*/
|
|
15
|
+
function hasRestrictedOutputLimit(model) {
|
|
16
|
+
if (!model) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Check for Gemini 3 models
|
|
20
|
+
if (model.includes("gemini-3")) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
// Check for image generation models (includes gemini-2.5-flash-image)
|
|
24
|
+
if (IMAGE_GENERATION_MODELS.some((m) => model.includes(m))) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
7
29
|
/**
|
|
8
30
|
* Get the safe maximum tokens for a provider and model
|
|
9
31
|
*/
|
|
10
32
|
export function getSafeMaxTokens(provider, model, requestedMaxTokens) {
|
|
33
|
+
// CRITICAL: Gemini 3 models AND image generation models have a hard limit of 32768 output tokens
|
|
34
|
+
// This check must happen FIRST, before any other logic, because these models
|
|
35
|
+
// will reject requests with maxOutputTokens > 32768
|
|
36
|
+
const isRestrictedModel = hasRestrictedOutputLimit(model);
|
|
37
|
+
if (isRestrictedModel) {
|
|
38
|
+
// Explicit undefined/null check so a caller-supplied 0 is preserved
|
|
39
|
+
// (truthy checks would treat 0 as "unset" and silently fall back to the cap).
|
|
40
|
+
if (requestedMaxTokens !== undefined &&
|
|
41
|
+
requestedMaxTokens !== null &&
|
|
42
|
+
requestedMaxTokens > GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS) {
|
|
43
|
+
logger.warn(`Requested maxTokens ${requestedMaxTokens} exceeds ${model} limit of ${GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS}. Using ${GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS} instead.`);
|
|
44
|
+
return GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS;
|
|
45
|
+
}
|
|
46
|
+
// If no maxTokens specified, use the restricted limit as default
|
|
47
|
+
if (requestedMaxTokens === undefined || requestedMaxTokens === null) {
|
|
48
|
+
return GEMINI_RESTRICTED_MAX_OUTPUT_TOKENS;
|
|
49
|
+
}
|
|
50
|
+
// Otherwise, use the requested value (it's within limits, including 0)
|
|
51
|
+
return requestedMaxTokens;
|
|
52
|
+
}
|
|
11
53
|
// Get provider-specific limits
|
|
12
54
|
const providerLimits = PROVIDER_MAX_TOKENS[provider];
|
|
13
55
|
if (!providerLimits) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.61.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"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.",
|
|
6
6
|
"author": {
|