@juspay/neurolink 11.28.0 → 11.29.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.
@@ -1,9 +1,9 @@
1
1
  import { context, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
2
2
  import { directAgentTools } from "../agent/directTools.js";
3
3
  import { defaultProviderFor } from "../factories/mediaHandlerCatalog.js";
4
- import { isImageGenerationModel } from "./constants.js";
5
4
  import { MiddlewareFactory } from "../middleware/factory.js";
6
5
  import { modelSupports } from "../models/modelRegistry.js";
6
+ import { resolveRequestKind } from "./resolveRequestKind.js";
7
7
  import { ATTR, tracers } from "../telemetry/index.js";
8
8
  import { ERROR_CODES, isAbortError, NeuroLinkError, } from "../utils/errorHandling.js";
9
9
  import { ProviderError } from "../types/index.js";
@@ -239,14 +239,12 @@ export class BaseProvider {
239
239
  }
240
240
  // CRITICAL: Image generation models don't support real streaming
241
241
  // Force fake streaming for image models to ensure image output is yielded.
242
- // Skip this path when the caller explicitly requests non-image output (e.g.
243
- // JSON analysis) so dual-mode models like gemini-3.1-flash-image-preview
244
- // can still perform text/structured generation.
245
- const isImageModel = isImageGenerationModel(this.modelName);
246
- const requestsNonImageOutput = options.output?.format === "json" ||
247
- options.output?.format === "structured" ||
248
- options.output?.format === "text";
249
- if (isImageModel && !requestsNonImageOutput) {
242
+ // resolveRequestKind() skips this path when the caller explicitly requests
243
+ // non-image output (e.g. JSON analysis) so dual-mode models like
244
+ // gemini-3.1-flash-image-preview can still perform text/structured
245
+ // generation see its doc comment for the full precedence table.
246
+ const requestKind = resolveRequestKind(options, this.modelName);
247
+ if (requestKind === "image") {
250
248
  logger.info(`Image model detected, forcing fake streaming`, {
251
249
  provider: this.providerName,
252
250
  model: this.modelName,
@@ -1067,14 +1065,13 @@ export class BaseProvider {
1067
1065
  }
1068
1066
  async runGenerateInActiveContext(options, startTime, otelSpan, otelSpanState) {
1069
1067
  try {
1070
- if (options.output?.mode === "video") {
1068
+ // Single source of truth for "what kind of request is this" — see
1069
+ // resolveRequestKind's doc comment for the full precedence table.
1070
+ const requestKind = resolveRequestKind(options, this.modelName);
1071
+ if (requestKind === "video") {
1071
1072
  return await this.handleVideoGeneration(options, startTime);
1072
1073
  }
1073
- const isImageModel = isImageGenerationModel(this.modelName);
1074
- const requestsNonImageOutput = options.output?.format === "json" ||
1075
- options.output?.format === "structured" ||
1076
- options.output?.format === "text";
1077
- if (isImageModel && !requestsNonImageOutput) {
1074
+ if (requestKind === "image") {
1078
1075
  logger.info(`Image generation model detected, routing to executeImageGeneration`, {
1079
1076
  provider: this.providerName,
1080
1077
  model: this.modelName,
@@ -1082,7 +1079,7 @@ export class BaseProvider {
1082
1079
  const imageResult = await this.executeImageGeneration(options);
1083
1080
  return await this.enhanceResult(imageResult, options, startTime);
1084
1081
  }
1085
- if (options.tts?.enabled && !options.tts?.useAiResponse) {
1082
+ if (requestKind === "tts-direct") {
1086
1083
  return this.handleDirectTTSSynthesis(options, startTime);
1087
1084
  }
1088
1085
  const { tools, model } = await this.prepareGenerationContext(options);
@@ -2179,7 +2176,12 @@ export class BaseProvider {
2179
2176
  // shared timeout helper so standard video gen honors the caller's
2180
2177
  // timeout the same way director mode does (see above ~Line 2062).
2181
2178
  const videoTimeout = options.timeout ?? 600_000; // 10 min default
2182
- const videoResult = await this.executeWithTimeout(() => VideoProcessor.generate(requestedProvider, imageBuffer, prompt, options.output?.video ?? {}, options.region), { timeout: videoTimeout, operationType: "generate" });
2179
+ const videoResult = await this.executeWithTimeout(() => VideoProcessor.generate(requestedProvider, {
2180
+ ...(options.output?.video ?? {}),
2181
+ image: imageBuffer,
2182
+ prompt,
2183
+ region: options.region,
2184
+ }), { timeout: videoTimeout, operationType: "generate" });
2183
2185
  // Prefer the handler's own model id (more accurate — it knows the exact
2184
2186
  // checkpoint that ran). Fall back to the request-time value, and finally
2185
2187
  // to the Vertex default only when we're on the Vertex route.
@@ -0,0 +1,26 @@
1
+ import type { RequestKind, RequestKindInput } from "../types/index.js";
2
+ /**
3
+ * The dispatch decision for "what kind of request is this" — text, image,
4
+ * video, music, avatar, direct TTS synthesis, or PPT generation — at the
5
+ * CORE call sites: neurolink.ts's maybeHandleEarlyGenerateResult
6
+ * (music/avatar/ppt/workflow routing) and baseProvider.ts's
7
+ * stream()/runGenerateInActiveContext (image/video/tts-direct routing) call
8
+ * this instead of independently re-deriving the decision.
9
+ *
10
+ * Also the only copy at the provider-override level: replicate.ts's
11
+ * generate() override and googleVertex/client.ts's generate()/stream()
12
+ * overrides (which bypass BaseProvider's paths) call this too, so an edit
13
+ * to this precedence table reaches every dispatch site.
14
+ *
15
+ * Precedence, checked in order:
16
+ * 1. output.mode (music/avatar/video/ppt) — an explicit mode always wins.
17
+ * 2. an image-generation model, unless the caller explicitly asked for a
18
+ * non-image output.format (json/structured/text) — this lets dual-mode
19
+ * models like gemini-3.1-flash-image-preview still perform text or
20
+ * structured generation when requested.
21
+ * 3. tts.enabled without tts.useAiResponse — direct synthesis, bypassing
22
+ * the LLM turn entirely (useAiResponse means the LLM's own text
23
+ * response gets synthesized afterward, which is NOT this branch).
24
+ * 4. otherwise, "text".
25
+ */
26
+ export declare function resolveRequestKind(options: RequestKindInput, modelName?: string): RequestKind;
@@ -0,0 +1,49 @@
1
+ import { isImageGenerationModel } from "./constants.js";
2
+ /**
3
+ * The dispatch decision for "what kind of request is this" — text, image,
4
+ * video, music, avatar, direct TTS synthesis, or PPT generation — at the
5
+ * CORE call sites: neurolink.ts's maybeHandleEarlyGenerateResult
6
+ * (music/avatar/ppt/workflow routing) and baseProvider.ts's
7
+ * stream()/runGenerateInActiveContext (image/video/tts-direct routing) call
8
+ * this instead of independently re-deriving the decision.
9
+ *
10
+ * Also the only copy at the provider-override level: replicate.ts's
11
+ * generate() override and googleVertex/client.ts's generate()/stream()
12
+ * overrides (which bypass BaseProvider's paths) call this too, so an edit
13
+ * to this precedence table reaches every dispatch site.
14
+ *
15
+ * Precedence, checked in order:
16
+ * 1. output.mode (music/avatar/video/ppt) — an explicit mode always wins.
17
+ * 2. an image-generation model, unless the caller explicitly asked for a
18
+ * non-image output.format (json/structured/text) — this lets dual-mode
19
+ * models like gemini-3.1-flash-image-preview still perform text or
20
+ * structured generation when requested.
21
+ * 3. tts.enabled without tts.useAiResponse — direct synthesis, bypassing
22
+ * the LLM turn entirely (useAiResponse means the LLM's own text
23
+ * response gets synthesized afterward, which is NOT this branch).
24
+ * 4. otherwise, "text".
25
+ */
26
+ export function resolveRequestKind(options, modelName) {
27
+ if (options.output?.mode === "music") {
28
+ return "music";
29
+ }
30
+ if (options.output?.mode === "avatar") {
31
+ return "avatar";
32
+ }
33
+ if (options.output?.mode === "video") {
34
+ return "video";
35
+ }
36
+ if (options.output?.mode === "ppt") {
37
+ return "ppt";
38
+ }
39
+ const requestsNonImageOutput = options.output?.format === "json" ||
40
+ options.output?.format === "structured" ||
41
+ options.output?.format === "text";
42
+ if (isImageGenerationModel(modelName) && !requestsNonImageOutput) {
43
+ return "image";
44
+ }
45
+ if (options.tts?.enabled && !options.tts?.useAiResponse) {
46
+ return "tts-direct";
47
+ }
48
+ return "text";
49
+ }
package/dist/neurolink.js CHANGED
@@ -34,6 +34,7 @@ import { buildToolRoutingCatalog, buildRoutingQueryFromHistory, resolveToolRouti
34
34
  import { ToolRoutingCache } from "./core/toolRoutingCache.js";
35
35
  import { DEFAULT_RECENT_TURNS, KnowledgeGroundingEngine, } from "./knowledge/index.js";
36
36
  import { AIProviderFactory } from "./core/factory.js";
37
+ import { resolveRequestKind } from "./core/resolveRequestKind.js";
37
38
  import { createToolEventPayload } from "./core/toolEvents.js";
38
39
  import { ProviderFactory } from "./factories/providerFactory.js";
39
40
  import { ProviderRegistry } from "./factories/providerRegistry.js";
@@ -3576,13 +3577,16 @@ Current user's request: ${currentInput}`;
3576
3577
  }
3577
3578
  return this.generateWithWorkflow(options);
3578
3579
  }
3579
- if (options.output?.mode === "music") {
3580
+ // Single source of truth for "what kind of request is this" — see
3581
+ // resolveRequestKind's doc comment for the full precedence table.
3582
+ const requestKind = resolveRequestKind(options, options.model);
3583
+ if (requestKind === "music") {
3580
3584
  return this.generateWithMusic(options, generateSpan);
3581
3585
  }
3582
- if (options.output?.mode === "avatar") {
3586
+ if (requestKind === "avatar") {
3583
3587
  return this.generateWithAvatar(options, generateSpan);
3584
3588
  }
3585
- if (options.output?.mode !== "ppt") {
3589
+ if (requestKind !== "ppt") {
3586
3590
  return null;
3587
3591
  }
3588
3592
  if (options.stt?.enabled && options.stt?.audio) {
@@ -8,7 +8,8 @@ import { BaseProvider } from "../../core/baseProvider.js";
8
8
  import { unwrapImagePayload } from "../../adapters/imageFormatSupport.js";
9
9
  import { appendNativeAudioParts } from "../googleNativeGemini3/utils.js";
10
10
  import { getMimeTypeForExtension } from "../../processors/config/mimeConstants.js";
11
- import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_EXECUTION_TIMEOUT_MS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../../core/constants.js";
11
+ import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_EXECUTION_TIMEOUT_MS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../../core/constants.js";
12
+ import { resolveRequestKind } from "../../core/resolveRequestKind.js";
12
13
  import { ModelConfigurationManager } from "../../core/modelConfiguration.js";
13
14
  import { isSchemaComplexityError } from "../../core/modules/structuredOutputPolicy.js";
14
15
  import { redactUrlForError, stringifyContentSafe, } from "../../utils/logSanitize.js";
@@ -5089,9 +5090,11 @@ export class GoogleVertexProvider extends BaseProvider {
5089
5090
  ? { input: { text: optionsOrPrompt } }
5090
5091
  : optionsOrPrompt;
5091
5092
  const modelName = options.model || this.modelName || getDefaultVertexModel();
5092
- // Check if this is an image generation model - image models don't support streaming
5093
- const isImageModel = IMAGE_GENERATION_MODELS.some((m) => modelName.toLowerCase().startsWith(m.toLowerCase()));
5094
- if (isImageModel) {
5093
+ // Image-generation requests can't stream fall back to generate.
5094
+ // Same single dispatch decision as generate(): resolveRequestKind
5095
+ // keeps dual-mode models streaming text when the caller explicitly
5096
+ // asked for a non-image output.format.
5097
+ if (resolveRequestKind(options, modelName) === "image") {
5095
5098
  logger.warn("[GoogleVertex] Image generation models don't support streaming, falling back to generate", { model: modelName });
5096
5099
  // Convert stream options to text generation options
5097
5100
  const generateOptions = {
@@ -5150,13 +5153,20 @@ export class GoogleVertexProvider extends BaseProvider {
5150
5153
  },
5151
5154
  }, async (generateSpan) => {
5152
5155
  const generateStartTime = Date.now();
5156
+ // One dispatch decision for the whole override: Vertex's generate()
5157
+ // bypasses BaseProvider.generate(), so it re-runs the same
5158
+ // resolveRequestKind() the core call sites use rather than keeping
5159
+ // a hand-rolled copy of the precedence (which had drifted: tts
5160
+ // checked before image, and a cruder case-insensitive startsWith
5161
+ // image match without boundary awareness).
5162
+ const requestKind = resolveRequestKind(options, modelName);
5153
5163
  // Video-mode requests must route through BaseProvider's
5154
5164
  // handleVideoGeneration (which loads the Veo 3 adapter). Vertex's
5155
5165
  // native @google/genai path is text/image only — without this
5156
5166
  // gate, video requests fall through to gemini-2.5-flash and the
5157
5167
  // model politely declines ("I cannot create animations") instead
5158
5168
  // of producing video bytes.
5159
- if (options.output?.mode === "video") {
5169
+ if (requestKind === "video") {
5160
5170
  logger.info("[GoogleVertex] Routing video-mode generate to handleVideoGeneration", { model: modelName });
5161
5171
  const videoResult = await this.handleVideoGeneration(options, generateStartTime);
5162
5172
  this.attachUsageAndCostAttributes(generateSpan, modelName, videoResult?.usage);
@@ -5168,15 +5178,17 @@ export class GoogleVertexProvider extends BaseProvider {
5168
5178
  // (synthesise the input text directly; no LLM call). BaseProvider's
5169
5179
  // standard generate() does the same dispatch — we replicate it here
5170
5180
  // because Vertex's override bypasses that path.
5171
- if (options.tts?.enabled && !options.tts?.useAiResponse) {
5181
+ if (requestKind === "tts-direct") {
5172
5182
  logger.info("[GoogleVertex] Routing TTS direct-synthesis to handleDirectTTSSynthesis", { model: modelName });
5173
5183
  const ttsResult = await this.handleDirectTTSSynthesis(options, generateStartTime);
5174
5184
  this.emitGenerationEnd(modelName, ttsResult, generateStartTime, true);
5175
5185
  return ttsResult;
5176
5186
  }
5177
- // Check if this is an image generation model - route to executeImageGeneration without tools
5178
- const isImageModel = IMAGE_GENERATION_MODELS.some((m) => modelName.toLowerCase().startsWith(m.toLowerCase()));
5179
- if (isImageModel) {
5187
+ // Image-generation models route to executeImageGeneration without
5188
+ // tools. resolveRequestKind also carries the dual-mode exception:
5189
+ // an explicit non-image output.format keeps models like
5190
+ // gemini-3.1-flash-image-preview on the text path.
5191
+ if (requestKind === "image") {
5180
5192
  logger.info("[GoogleVertex] Routing image generation model to executeImageGeneration", { model: modelName });
5181
5193
  const imageResult = await this.executeImageGeneration(options);
5182
5194
  this.attachUsageAndCostAttributes(generateSpan, modelName, imageResult?.usage);
@@ -1,5 +1,6 @@
1
1
  import { ErrorCategory, ErrorSeverity, ReplicateModels, } from "../constants/enums.js";
2
2
  import { BaseProvider } from "../core/baseProvider.js";
3
+ import { resolveRequestKind } from "../core/resolveRequestKind.js";
3
4
  import { getReplicateAuth } from "../adapters/replicate/auth.js";
4
5
  import { downloadPredictionOutput, predict, } from "../adapters/replicate/predictionLifecycle.js";
5
6
  import { MAX_IMAGE_BYTES } from "../utils/sizeGuard.js";
@@ -121,19 +122,18 @@ export class ReplicateProvider extends BaseProvider {
121
122
  const options = typeof optionsOrPrompt === "string"
122
123
  ? { prompt: optionsOrPrompt }
123
124
  : optionsOrPrompt;
124
- const { isImageGenerationModel } = await import("../core/constants.js");
125
- // Delegate special output modes to base class (which never calls getAISDKModel for these)
126
- if (options.output?.mode === "video" ||
127
- options.output?.mode === "avatar" ||
128
- options.output?.mode === "music") {
129
- return super.generate(options, _analysisSchema);
130
- }
131
- // Image-gen models: delegate to base which calls executeImageGeneration()
132
- const isImageModel = isImageGenerationModel(this.modelName);
133
- const requestsNonImageOutput = options.output?.format === "json" ||
134
- options.output?.format === "structured" ||
135
- options.output?.format === "text";
136
- if (isImageModel && !requestsNonImageOutput) {
125
+ // Delegate media kinds to the base class (which never calls
126
+ // getAISDKModel for these). resolveRequestKind owns the precedence
127
+ // including the dual-mode exception where an explicit non-image
128
+ // output.format keeps an image-gen model on the text path. "ppt" and
129
+ // "tts-direct" deliberately stay on the local text path below:
130
+ // super.generate() would hit prepareGenerationContext()
131
+ // getAISDKModel(), which throws for Replicate.
132
+ const kind = resolveRequestKind(options, this.modelName);
133
+ if (kind === "video" ||
134
+ kind === "avatar" ||
135
+ kind === "music" ||
136
+ kind === "image") {
137
137
  return super.generate(options, _analysisSchema);
138
138
  }
139
139
  // Structured / JSON output is not natively supported by the Replicate
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Types backing resolveRequestKind() (src/lib/core/resolveRequestKind.ts) —
3
+ * the single function that decides which of NeuroLink's output modes a
4
+ * generate/stream request is asking for.
5
+ */
6
+ export type RequestKind = "text" | "image" | "video" | "music" | "avatar" | "tts-direct" | "ppt";
7
+ /**
8
+ * Narrow structural subset of TextGenerationOptions/GenerateOptions that
9
+ * resolveRequestKind() actually reads. Kept intentionally minimal (rather
10
+ * than importing the full options type) so this module has no dependency
11
+ * on the wider options type graph.
12
+ */
13
+ export type RequestKindInput = {
14
+ output?: {
15
+ mode?: string;
16
+ format?: string;
17
+ };
18
+ tts?: {
19
+ enabled?: boolean;
20
+ useAiResponse?: boolean;
21
+ };
22
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Types backing resolveRequestKind() (src/lib/core/resolveRequestKind.ts) —
3
+ * the single function that decides which of NeuroLink's output modes a
4
+ * generate/stream request is asking for.
5
+ */
6
+ export {};
@@ -802,23 +802,12 @@ export type ToolExecutionCaptureOptions = {
802
802
  */
803
803
  export type GenerateStopReason = "completed" | "step-cap" | "context-cap" | "time-limit" | "stalled" | "aborted" | "provider-error";
804
804
  /**
805
- * Generate function result type - Primary output format
806
- * Future-ready for multi-modal outputs while maintaining text focus
805
+ * Media generation/processing outputs shared by GenerateResult and
806
+ * TextGenerationResult. Extracted so both result types intersect (&) this
807
+ * single definition instead of each declaring its own drifting copy of the
808
+ * same audio/video/avatar/music/ppt/image/transcription fields.
807
809
  */
808
- export type GenerateResult = {
809
- content: string;
810
- /** Knowledge-grounding diagnostics for this turn (present only when grounding ran). */
811
- knowledge?: KnowledgeGroundingMetadata;
812
- /**
813
- * Parsed structured object when a `schema` was requested. Populated from
814
- * AI-SDK experimental_output, or from text-mode coercion (balanced-scan +
815
- * jsonrepair). Prefer this over JSON.parse(content) — it never requires the
816
- * caller to re-parse hand-escaped model text.
817
- */
818
- structuredData?: unknown;
819
- outputs?: {
820
- text: string;
821
- };
810
+ export type MediaGenerationOutputs = {
822
811
  /**
823
812
  * Text-to-Speech audio result
824
813
  *
@@ -911,9 +900,31 @@ export type GenerateResult = {
911
900
  * ```
912
901
  */
913
902
  ppt?: PPTGenerationResult;
903
+ /** Standard format for image generation */
914
904
  imageOutput?: {
915
905
  base64: string;
916
906
  } | null;
907
+ /** STT transcription result (present when stt.enabled is true and audio input was provided) */
908
+ transcription?: STTResult;
909
+ };
910
+ /**
911
+ * Generate function result type - Primary output format
912
+ * Future-ready for multi-modal outputs while maintaining text focus
913
+ */
914
+ export type GenerateResult = {
915
+ content: string;
916
+ /** Knowledge-grounding diagnostics for this turn (present only when grounding ran). */
917
+ knowledge?: KnowledgeGroundingMetadata;
918
+ /**
919
+ * Parsed structured object when a `schema` was requested. Populated from
920
+ * AI-SDK experimental_output, or from text-mode coercion (balanced-scan +
921
+ * jsonrepair). Prefer this over JSON.parse(content) — it never requires the
922
+ * caller to re-parse hand-escaped model text.
923
+ */
924
+ structuredData?: unknown;
925
+ outputs?: {
926
+ text: string;
927
+ };
917
928
  provider?: string;
918
929
  model?: string;
919
930
  finishReason?: string;
@@ -1013,8 +1024,6 @@ export type GenerateResult = {
1013
1024
  reasoning?: string;
1014
1025
  /** Token count for reasoning content */
1015
1026
  reasoningTokens?: number;
1016
- /** STT transcription result (present when stt.enabled is true and audio input was provided) */
1017
- transcription?: STTResult;
1018
1027
  retries?: {
1019
1028
  count: number;
1020
1029
  errors: Array<{
@@ -1033,7 +1042,7 @@ export type GenerateResult = {
1033
1042
  * absolute `requestsRemaining` / `tokensRemaining`.
1034
1043
  */
1035
1044
  limits?: ClaudeLimitSnapshot;
1036
- };
1045
+ } & MediaGenerationOutputs;
1037
1046
  /**
1038
1047
  * Unified options for both generation and streaming
1039
1048
  * Supports factory patterns and domain configuration
@@ -1467,23 +1476,6 @@ export type TextGenerationResult = {
1467
1476
  }>;
1468
1477
  analytics?: AnalyticsData;
1469
1478
  evaluation?: EvaluationData;
1470
- audio?: TTSResult;
1471
- /** Outcome of TTS synthesis, including the failure reason. */
1472
- ttsMetadata?: TTSMetadata;
1473
- /** STT transcription result (present when stt input was processed) */
1474
- transcription?: STTResult;
1475
- /** Video generation result */
1476
- video?: VideoGenerationResult;
1477
- /** Avatar (talking-head) generation result */
1478
- avatar?: AvatarResult;
1479
- /** Music generation result */
1480
- music?: MusicResult;
1481
- /** PowerPoint generation result */
1482
- ppt?: PPTGenerationResult;
1483
- /** Image generation output */
1484
- imageOutput?: {
1485
- base64: string;
1486
- } | null;
1487
1479
  /** Gemini 3 thought signature for reasoning continuity across turns */
1488
1480
  thoughtSignature?: string;
1489
1481
  /** Thinking/reasoning text from provider (Anthropic thinking blocks, Gemini thought parts, DeepSeek/NIM reasoning_content) */
@@ -1497,7 +1489,7 @@ export type TextGenerationResult = {
1497
1489
  message: string;
1498
1490
  }>;
1499
1491
  };
1500
- };
1492
+ } & MediaGenerationOutputs;
1501
1493
  /**
1502
1494
  * Enhanced result type with optional analytics/evaluation
1503
1495
  */
@@ -86,3 +86,4 @@ export * from "./requestRouter.js";
86
86
  export * from "./classifierRouter.js";
87
87
  export * from "./agentNetwork.js";
88
88
  export * from "./localUsage.js";
89
+ export * from "./dispatch.js";
@@ -97,3 +97,5 @@ export * from "./classifierRouter.js";
97
97
  // Multi-Agent orchestration types
98
98
  export * from "./agentNetwork.js";
99
99
  export * from "./localUsage.js";
100
+ // resolveRequestKind() dispatch-decision types
101
+ export * from "./dispatch.js";
@@ -13,6 +13,19 @@
13
13
  */
14
14
  import type { VideoGenerationResult, VideoOutputOptions } from "./multimodal.js";
15
15
  export type { VideoGenerationResult, VideoOutputOptions, } from "./multimodal.js";
16
+ /**
17
+ * Bag-form input to `VideoProcessor.generate()` — the primary data (image,
18
+ * prompt, region) alongside the video-specific output options, collapsed
19
+ * into a single object matching Music/Avatar's existing `generate(provider,
20
+ * options)` shape. `VideoHandler.generate()`'s own 4-positional-argument
21
+ * signature is unchanged; `VideoProcessor.generate()` translates between the
22
+ * two internally.
23
+ */
24
+ export type VideoGenerateOptions = VideoOutputOptions & {
25
+ image: Buffer;
26
+ prompt: string;
27
+ region?: string;
28
+ };
16
29
  /**
17
30
  * Director-mode transition options.
18
31
  *
@@ -11,7 +11,7 @@
11
11
  * @module utils/videoProcessor
12
12
  */
13
13
  import { VIDEO_ERROR_CODES } from "../constants/videoErrors.js";
14
- import type { VideoGenerationResult, VideoHandler, VideoOutputOptions, VideoTransitionOptions } from "../types/index.js";
14
+ import type { VideoGenerateOptions, VideoGenerationResult, VideoHandler, VideoOutputOptions, VideoTransitionOptions } from "../types/index.js";
15
15
  import { VideoError } from "../adapters/video/vertexVideoHandler.js";
16
16
  export { VideoError, VIDEO_ERROR_CODES };
17
17
  /**
@@ -45,13 +45,19 @@ export declare class VideoProcessor {
45
45
  * Generate a single video clip via the registered handler.
46
46
  *
47
47
  * @param provider - Registered provider name (e.g. "vertex", "kling")
48
- * @param image - Source image buffer
49
- * @param prompt - Text prompt describing the desired motion / content
50
- * @param options - Resolution / length / aspect-ratio / audio options
51
- * @param region - Optional region override (Vertex location, etc.)
48
+ * @param options - Bag of the source image, prompt, optional region
49
+ * override, and resolution / length / aspect-ratio / audio options.
50
+ * Translated internally into the handler-level 4-positional-argument
51
+ * call `VideoHandler.generate()`'s own signature is unchanged.
52
52
  * @throws VideoError on registry miss, handler-not-configured, or
53
53
  * generation failure
54
54
  */
55
+ static generate(provider: string, options: VideoGenerateOptions): Promise<VideoGenerationResult>;
56
+ /**
57
+ * @deprecated Positional form kept for backward compatibility with
58
+ * pre-bag callers (VideoProcessor is a public export). Use the
59
+ * options-bag overload.
60
+ */
55
61
  static generate(provider: string, image: Buffer, prompt: string, options: VideoOutputOptions, region?: string): Promise<VideoGenerationResult>;
56
62
  /**
57
63
  * Generate a transition clip via the registered handler (Director Mode).
@@ -14,6 +14,11 @@ import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
14
14
  import { VIDEO_ERROR_CODES } from "../constants/videoErrors.js";
15
15
  import { SpanSerializer, SpanStatus, SpanType, getMetricsAggregator, } from "../observability/index.js";
16
16
  import { logger } from "./logger.js";
17
+ import { withTimeout } from "./async/withTimeout.js";
18
+ // Video generation is legitimately minutes-long (Kling/Runway render queues),
19
+ // so the bound is generous — its job is to convert a wedged handler into an
20
+ // error rather than an eternal hang, not to police normal latency.
21
+ const VIDEO_GENERATION_TIMEOUT_MS = 600_000;
17
22
  // VideoError is canonical in vertexVideoHandler.ts (existing). Re-export
18
23
  // here so consumers of `VideoProcessor` can import the typed error from
19
24
  // the same module. Both throws and instanceof checks resolve to the same
@@ -69,19 +74,17 @@ export class VideoProcessor {
69
74
  "video.audio": options.audio,
70
75
  };
71
76
  }
72
- /**
73
- * Generate a single video clip via the registered handler.
74
- *
75
- * @param provider - Registered provider name (e.g. "vertex", "kling")
76
- * @param image - Source image buffer
77
- * @param prompt - Text prompt describing the desired motion / content
78
- * @param options - Resolution / length / aspect-ratio / audio options
79
- * @param region - Optional region override (Vertex location, etc.)
80
- * @throws VideoError on registry miss, handler-not-configured, or
81
- * generation failure
82
- */
83
- static async generate(provider, image, prompt, options, region) {
84
- const span = SpanSerializer.createSpan(SpanType.MEDIA_GENERATION, "video.generate", this.buildSpanAttributes(provider, options));
77
+ static async generate(provider, optionsOrImage, legacyPrompt, legacyOptions, legacyRegion) {
78
+ const bag = Buffer.isBuffer(optionsOrImage)
79
+ ? {
80
+ image: optionsOrImage,
81
+ prompt: legacyPrompt ?? "",
82
+ ...(legacyRegion !== undefined ? { region: legacyRegion } : {}),
83
+ ...(legacyOptions ?? {}),
84
+ }
85
+ : optionsOrImage;
86
+ const { image, prompt, region, ...videoOptions } = bag;
87
+ const span = SpanSerializer.createSpan(SpanType.MEDIA_GENERATION, "video.generate", this.buildSpanAttributes(provider, videoOptions));
85
88
  try {
86
89
  const handler = this.getHandler(provider);
87
90
  if (!handler) {
@@ -105,7 +108,10 @@ export class VideoProcessor {
105
108
  });
106
109
  }
107
110
  logger.debug(`[VideoProcessor] Starting video generation with provider: ${provider}`);
108
- const result = await handler.generate(image, prompt, options, region);
111
+ // Bounded per repo guideline (async provider calls wrap withTimeout):
112
+ // video generation is legitimately slow, so the deadline is generous —
113
+ // but a wedged handler must error, never hang the caller forever.
114
+ const result = await withTimeout(handler.generate(image, prompt, videoOptions, region), VIDEO_GENERATION_TIMEOUT_MS, `Video generation via "${provider}" timed out after ${VIDEO_GENERATION_TIMEOUT_MS}ms`);
109
115
  const ended = SpanSerializer.endSpan(span, SpanStatus.OK);
110
116
  getMetricsAggregator().recordSpan(ended);
111
117
  logger.info(`[VideoProcessor] Generated ${result.data.length} bytes (${provider})`);
@@ -124,7 +130,7 @@ export class VideoProcessor {
124
130
  category: ErrorCategory.EXECUTION,
125
131
  severity: ErrorSeverity.HIGH,
126
132
  retriable: true,
127
- context: { provider, options, region },
133
+ context: { provider, options: videoOptions, region },
128
134
  originalError: err instanceof Error ? err : undefined,
129
135
  });
130
136
  }
@@ -169,7 +175,8 @@ export class VideoProcessor {
169
175
  });
170
176
  }
171
177
  try {
172
- return await handler.generateTransition(firstFrame, lastFrame, prompt, options, region);
178
+ // Same bound as generate(): a wedged transition must error, not hang.
179
+ return await withTimeout(handler.generateTransition(firstFrame, lastFrame, prompt, options, region), VIDEO_GENERATION_TIMEOUT_MS, `Video transition via "${provider}" timed out after ${VIDEO_GENERATION_TIMEOUT_MS}ms`);
173
180
  }
174
181
  catch (err) {
175
182
  if (err instanceof VideoError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.28.0",
3
+ "version": "11.29.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -73,6 +73,7 @@
73
73
  "test:context": "pnpm exec tsx test/continuous-test-suite-context.ts",
74
74
  "test:evaluation": "pnpm exec tsx test/continuous-test-suite-evaluation.ts",
75
75
  "test:handler-registry": "pnpm exec tsx test/continuous-test-suite-handler-registry.ts",
76
+ "test:resolve-request-kind": "pnpm exec tsx test/continuous-test-suite-resolve-request-kind.ts",
76
77
  "test:mcp": "pnpm exec tsx test/continuous-test-suite-mcp-infra.ts",
77
78
  "test:tool-resolution": "pnpm exec tsx test/continuous-test-suite-tool-resolution.ts",
78
79
  "test:mcp:http": "pnpm exec tsx test/continuous-test-suite-mcp-http.ts",