@juspay/neurolink 9.59.4 → 9.59.6

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.
@@ -570,6 +570,19 @@ export class BaseProvider {
570
570
  if (!hasVideoFrames(messages)) {
571
571
  return null;
572
572
  }
573
+ // Bug 2 fix: callers requesting structured output (schema or explicit
574
+ // output.format) must NOT be hijacked into the prose-returning video
575
+ // analysis path. Without this gate, schema/format are silently dropped
576
+ // whenever messages contain >=3 image parts.
577
+ if (options.schema !== undefined || options.output?.format !== undefined) {
578
+ logger.info("[VideoFrameGen] Skipping video-frame analysis route; caller requested structured output", {
579
+ provider: this.providerName,
580
+ model: this.modelName,
581
+ hasSchema: options.schema !== undefined,
582
+ outputFormat: options.output?.format,
583
+ });
584
+ return null;
585
+ }
573
586
  const videoAnalysisResult = await executeVideoAnalysis(messages, {
574
587
  provider: options.provider,
575
588
  providerName: this.providerName,
@@ -570,6 +570,19 @@ export class BaseProvider {
570
570
  if (!hasVideoFrames(messages)) {
571
571
  return null;
572
572
  }
573
+ // Bug 2 fix: callers requesting structured output (schema or explicit
574
+ // output.format) must NOT be hijacked into the prose-returning video
575
+ // analysis path. Without this gate, schema/format are silently dropped
576
+ // whenever messages contain >=3 image parts.
577
+ if (options.schema !== undefined || options.output?.format !== undefined) {
578
+ logger.info("[VideoFrameGen] Skipping video-frame analysis route; caller requested structured output", {
579
+ provider: this.providerName,
580
+ model: this.modelName,
581
+ hasSchema: options.schema !== undefined,
582
+ outputFormat: options.output?.format,
583
+ });
584
+ return null;
585
+ }
573
586
  const videoAnalysisResult = await executeVideoAnalysis(messages, {
574
587
  provider: options.provider,
575
588
  providerName: this.providerName,
@@ -7255,7 +7255,7 @@ Current user's request: ${currentInput}`;
7255
7255
  const finalOptions = {
7256
7256
  timeout: options?.timeout ??
7257
7257
  toolInfo?.tool?.timeoutMs ??
7258
- TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS,
7258
+ TOOL_TIMEOUTS.EXECUTION_BATCH_MS,
7259
7259
  maxRetries: options?.maxRetries ??
7260
7260
  toolInfo?.tool?.maxRetries ??
7261
7261
  RETRY_ATTEMPTS.DEFAULT,
@@ -311,7 +311,14 @@ export declare class GoogleVertexProvider extends BaseProvider {
311
311
  */
312
312
  private buildImageGenerationParts;
313
313
  /**
314
- * Parse the Vertex AI image generation REST API response and extract image data.
314
+ * Parse the Vertex AI image generation REST API response.
315
+ *
316
+ * Dual-mode image models (gemini-3.1-flash-image-preview, gemini-2.5-flash-image,
317
+ * gemini-3-pro-image-preview) decide per-request whether to emit an image or text.
318
+ * When the response contains text parts but no image part, surface the text via
319
+ * `textFallback` so the caller can return a normal text result instead of throwing
320
+ * "model returned text instead of image data" and burning retries on a query that
321
+ * the model has already answered.
315
322
  */
316
323
  private parseImageGenerationResponse;
317
324
  /**
@@ -3015,7 +3015,14 @@ export class GoogleVertexProvider extends BaseProvider {
3015
3015
  return parts;
3016
3016
  }
3017
3017
  /**
3018
- * Parse the Vertex AI image generation REST API response and extract image data.
3018
+ * Parse the Vertex AI image generation REST API response.
3019
+ *
3020
+ * Dual-mode image models (gemini-3.1-flash-image-preview, gemini-2.5-flash-image,
3021
+ * gemini-3-pro-image-preview) decide per-request whether to emit an image or text.
3022
+ * When the response contains text parts but no image part, surface the text via
3023
+ * `textFallback` so the caller can return a normal text result instead of throwing
3024
+ * "model returned text instead of image data" and burning retries on a query that
3025
+ * the model has already answered.
3019
3026
  */
3020
3027
  parseImageGenerationResponse(data, imageModelName) {
3021
3028
  const candidate = data.candidates?.[0];
@@ -3030,10 +3037,15 @@ export class GoogleVertexProvider extends BaseProvider {
3030
3037
  (part.inline_data &&
3031
3038
  part.inline_data.mime_type?.startsWith("image/"))));
3032
3039
  if (!imagePart) {
3033
- const hasTextContent = candidate.content.parts.some((part) => part.text);
3034
- throw new ProviderError(hasTextContent
3035
- ? `Image generation completed but model returned text instead of image data. Model: ${imageModelName}`
3036
- : `Image generation completed but no image data was returned. Model: ${imageModelName}`, this.providerName);
3040
+ // Filter out empty/whitespace-only text parts so an effectively empty
3041
+ // response throws "no image data" instead of returning content: "".
3042
+ const textParts = candidate.content.parts
3043
+ .map((part) => (typeof part.text === "string" ? part.text : ""))
3044
+ .filter((text) => text.trim().length > 0);
3045
+ if (textParts.length > 0) {
3046
+ return { textFallback: textParts.join("") };
3047
+ }
3048
+ throw new ProviderError(`Image generation completed but no image data was returned. Model: ${imageModelName}`, this.providerName);
3037
3049
  }
3038
3050
  const imageData = imagePart.inlineData?.data || imagePart.inline_data?.data;
3039
3051
  const mimeType = imagePart.inlineData?.mimeType ||
@@ -3127,7 +3139,31 @@ export class GoogleVertexProvider extends BaseProvider {
3127
3139
  throw new ProviderError(`Vertex AI API error (${response.status}): ${errorText}`, this.providerName);
3128
3140
  }
3129
3141
  const data = (await response.json());
3130
- const { imageData, mimeType } = this.parseImageGenerationResponse(data, imageModelName);
3142
+ const parsed = this.parseImageGenerationResponse(data, imageModelName);
3143
+ // Dual-mode model decided to emit text instead of an image. Surface the
3144
+ // text as a normal text result instead of throwing — the model already
3145
+ // answered the user; failing here just burns retries.
3146
+ if ("textFallback" in parsed) {
3147
+ logger.info("Dual-mode image model returned text; returning as text result", {
3148
+ model: imageModelName,
3149
+ textLength: parsed.textFallback.length,
3150
+ responseTime: Date.now() - startTime,
3151
+ });
3152
+ const inputTokens = this.estimateTokenCount(prompt);
3153
+ const outputTokens = this.estimateTokenCount(parsed.textFallback);
3154
+ const textResult = {
3155
+ content: parsed.textFallback,
3156
+ provider: this.providerName,
3157
+ model: imageModelName,
3158
+ usage: {
3159
+ input: inputTokens,
3160
+ output: outputTokens,
3161
+ total: inputTokens + outputTokens,
3162
+ },
3163
+ };
3164
+ return await this.enhanceResult(textResult, options, startTime);
3165
+ }
3166
+ const { imageData, mimeType } = parsed;
3131
3167
  logger.info("Image generation successful", {
3132
3168
  model: imageModelName,
3133
3169
  mimeType,
package/dist/neurolink.js CHANGED
@@ -7255,7 +7255,7 @@ Current user's request: ${currentInput}`;
7255
7255
  const finalOptions = {
7256
7256
  timeout: options?.timeout ??
7257
7257
  toolInfo?.tool?.timeoutMs ??
7258
- TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS,
7258
+ TOOL_TIMEOUTS.EXECUTION_BATCH_MS,
7259
7259
  maxRetries: options?.maxRetries ??
7260
7260
  toolInfo?.tool?.maxRetries ??
7261
7261
  RETRY_ATTEMPTS.DEFAULT,
@@ -311,7 +311,14 @@ export declare class GoogleVertexProvider extends BaseProvider {
311
311
  */
312
312
  private buildImageGenerationParts;
313
313
  /**
314
- * Parse the Vertex AI image generation REST API response and extract image data.
314
+ * Parse the Vertex AI image generation REST API response.
315
+ *
316
+ * Dual-mode image models (gemini-3.1-flash-image-preview, gemini-2.5-flash-image,
317
+ * gemini-3-pro-image-preview) decide per-request whether to emit an image or text.
318
+ * When the response contains text parts but no image part, surface the text via
319
+ * `textFallback` so the caller can return a normal text result instead of throwing
320
+ * "model returned text instead of image data" and burning retries on a query that
321
+ * the model has already answered.
315
322
  */
316
323
  private parseImageGenerationResponse;
317
324
  /**
@@ -3015,7 +3015,14 @@ export class GoogleVertexProvider extends BaseProvider {
3015
3015
  return parts;
3016
3016
  }
3017
3017
  /**
3018
- * Parse the Vertex AI image generation REST API response and extract image data.
3018
+ * Parse the Vertex AI image generation REST API response.
3019
+ *
3020
+ * Dual-mode image models (gemini-3.1-flash-image-preview, gemini-2.5-flash-image,
3021
+ * gemini-3-pro-image-preview) decide per-request whether to emit an image or text.
3022
+ * When the response contains text parts but no image part, surface the text via
3023
+ * `textFallback` so the caller can return a normal text result instead of throwing
3024
+ * "model returned text instead of image data" and burning retries on a query that
3025
+ * the model has already answered.
3019
3026
  */
3020
3027
  parseImageGenerationResponse(data, imageModelName) {
3021
3028
  const candidate = data.candidates?.[0];
@@ -3030,10 +3037,15 @@ export class GoogleVertexProvider extends BaseProvider {
3030
3037
  (part.inline_data &&
3031
3038
  part.inline_data.mime_type?.startsWith("image/"))));
3032
3039
  if (!imagePart) {
3033
- const hasTextContent = candidate.content.parts.some((part) => part.text);
3034
- throw new ProviderError(hasTextContent
3035
- ? `Image generation completed but model returned text instead of image data. Model: ${imageModelName}`
3036
- : `Image generation completed but no image data was returned. Model: ${imageModelName}`, this.providerName);
3040
+ // Filter out empty/whitespace-only text parts so an effectively empty
3041
+ // response throws "no image data" instead of returning content: "".
3042
+ const textParts = candidate.content.parts
3043
+ .map((part) => (typeof part.text === "string" ? part.text : ""))
3044
+ .filter((text) => text.trim().length > 0);
3045
+ if (textParts.length > 0) {
3046
+ return { textFallback: textParts.join("") };
3047
+ }
3048
+ throw new ProviderError(`Image generation completed but no image data was returned. Model: ${imageModelName}`, this.providerName);
3037
3049
  }
3038
3050
  const imageData = imagePart.inlineData?.data || imagePart.inline_data?.data;
3039
3051
  const mimeType = imagePart.inlineData?.mimeType ||
@@ -3127,7 +3139,31 @@ export class GoogleVertexProvider extends BaseProvider {
3127
3139
  throw new ProviderError(`Vertex AI API error (${response.status}): ${errorText}`, this.providerName);
3128
3140
  }
3129
3141
  const data = (await response.json());
3130
- const { imageData, mimeType } = this.parseImageGenerationResponse(data, imageModelName);
3142
+ const parsed = this.parseImageGenerationResponse(data, imageModelName);
3143
+ // Dual-mode model decided to emit text instead of an image. Surface the
3144
+ // text as a normal text result instead of throwing — the model already
3145
+ // answered the user; failing here just burns retries.
3146
+ if ("textFallback" in parsed) {
3147
+ logger.info("Dual-mode image model returned text; returning as text result", {
3148
+ model: imageModelName,
3149
+ textLength: parsed.textFallback.length,
3150
+ responseTime: Date.now() - startTime,
3151
+ });
3152
+ const inputTokens = this.estimateTokenCount(prompt);
3153
+ const outputTokens = this.estimateTokenCount(parsed.textFallback);
3154
+ const textResult = {
3155
+ content: parsed.textFallback,
3156
+ provider: this.providerName,
3157
+ model: imageModelName,
3158
+ usage: {
3159
+ input: inputTokens,
3160
+ output: outputTokens,
3161
+ total: inputTokens + outputTokens,
3162
+ },
3163
+ };
3164
+ return await this.enhanceResult(textResult, options, startTime);
3165
+ }
3166
+ const { imageData, mimeType } = parsed;
3131
3167
  logger.info("Image generation successful", {
3132
3168
  model: imageModelName,
3133
3169
  mimeType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.59.4",
3
+ "version": "9.59.6",
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": {