@juspay/neurolink 12.26.2 → 12.27.0

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/dist/cli/index.js CHANGED
@@ -10,6 +10,7 @@ process.env.DOTENV_CONFIG_QUIET = "true";
10
10
  import { initializeCliParser } from "./parser.js";
11
11
  import chalk from "chalk";
12
12
  import { initializeCliLifecycle, cleanupCliLifecycle } from "./lifecycle.js";
13
+ import { loadProcessDotenv } from "../utils/dotenvBootstrap.js";
13
14
  // Clean up pnpm-specific environment variables that cause npm warnings
14
15
  // These variables are set by pnpm but cause "Unknown env config" warnings in npm
15
16
  if (process.env.npm_config_verify_deps_before_run) {
@@ -18,17 +19,11 @@ if (process.env.npm_config_verify_deps_before_run) {
18
19
  if (process.env.npm_config__jsr_registry) {
19
20
  delete process.env.npm_config__jsr_registry;
20
21
  }
21
- // Load environment variables from .env file
22
- // Suppress dotenv v17's stdout banner before importing — it pollutes CLI JSON output
23
- process.env.DOTENV_CONFIG_QUIET = "true";
24
- try {
25
- const { config } = await import("dotenv");
26
- config({ quiet: true });
27
- }
28
- catch {
29
- // dotenv is not available (dev dependency only) - this is fine for production
30
- // Environment variables should be set externally in production
31
- }
22
+ // Load environment variables from .env file. Shared with the SDK entry
23
+ // point; see dotenvBootstrap for why DOTENV_CONFIG_PATH has to be honoured
24
+ // here too — a suite that drives the built CLI inherits the stripped
25
+ // environment, and this is the process that would otherwise undo it.
26
+ await loadProcessDotenv();
32
27
  // Enhanced CLI with Professional UX
33
28
  const cli = initializeCliParser();
34
29
  initializeCliLifecycle(async () => {
package/dist/index.d.ts CHANGED
@@ -72,6 +72,7 @@ export { AzureTTS, AzureTTSHandler, CartesiaTTS, CartesiaTTSHandler, ElevenLabsT
72
72
  export { BeatovenMusic, BeatovenMusicHandler, ElevenLabsMusic, ElevenLabsMusicHandler, LyriaMusic, LyriaMusicHandler, registerDefaultMusicHandlers, ReplicateMusic, ReplicateMusicHandler, } from "./music/index.js";
73
73
  export { DIDAvatar, DIDAvatarHandler, HeyGenAvatar, HeyGenAvatarHandler, registerDefaultAvatarHandlers, ReplicateAvatar, ReplicateAvatarHandler, } from "./avatar/index.js";
74
74
  export { isVertexVideoConfigured, KlingVideoHandler, registerDefaultVideoHandlers, ReplicateVideoHandler, RunwayVideoHandler, VertexVideoHandler, } from "./adapters/video/index.js";
75
+ export { canDeliverVideoNatively, estimateVideoTokens, getVideoProviderConfig, isNativeVideoMimeType, supportsNativeVideo, VIDEO_PROVIDER_CONFIGS, } from "./adapters/videoFormatSupport.js";
75
76
  export { ImageGenService } from "./image-gen/ImageGenService.js";
76
77
  export { detectImageMimeType, sniffImageMimeType, } from "./utils/imageDetection.js";
77
78
  export { HITLManager } from "./hitl/hitlManager.js";
package/dist/index.js CHANGED
@@ -145,6 +145,13 @@ export { BeatovenMusic, BeatovenMusicHandler, ElevenLabsMusic, ElevenLabsMusicHa
145
145
  export { DIDAvatar, DIDAvatarHandler, HeyGenAvatar, HeyGenAvatarHandler, registerDefaultAvatarHandlers, ReplicateAvatar, ReplicateAvatarHandler, } from "./avatar/index.js";
146
146
  // Video handlers
147
147
  export { isVertexVideoConfigured, KlingVideoHandler, registerDefaultVideoHandlers, ReplicateVideoHandler, RunwayVideoHandler, VertexVideoHandler, } from "./adapters/video/index.js";
148
+ // Video *ingestion* capabilities — what each provider does with an attached
149
+ // clip. Surfaced because the answer changes what a caller should send: a
150
+ // provider on the native path is billed by duration and reads the audio
151
+ // track, one on the frame path is billed per keyframe and hears nothing, and
152
+ // without a way to ask, the only way to find out is to send a video and read
153
+ // the reply.
154
+ export { canDeliverVideoNatively, estimateVideoTokens, getVideoProviderConfig, isNativeVideoMimeType, supportsNativeVideo, VIDEO_PROVIDER_CONFIGS, } from "./adapters/videoFormatSupport.js";
148
155
  // Image generation + HITL — surfaced from their dedicated barrels
149
156
  export { ImageGenService } from "./image-gen/ImageGenService.js";
150
157
  export { detectImageMimeType, sniffImageMimeType, } from "./utils/imageDetection.js";
package/dist/neurolink.js CHANGED
@@ -5,16 +5,12 @@
5
5
  * Enhanced AI provider system with natural MCP tool access.
6
6
  * Uses real MCP infrastructure for tool discovery and execution.
7
7
  */
8
- // Load environment variables from .env file (critical for SDK usage)
9
- // Suppress dotenv v17 stdout banner — it pollutes CLI JSON output
10
- try {
11
- process.env.DOTENV_CONFIG_QUIET = process.env.DOTENV_CONFIG_QUIET ?? "true";
12
- const { config: dotenvConfig } = await import("dotenv");
13
- dotenvConfig({ quiet: true });
14
- }
15
- catch {
16
- // Environment variables should be set externally in production
17
- }
8
+ // Load environment variables from .env file (critical for SDK usage).
9
+ // Shared with the CLI so the two implicit loads cannot drift apart again.
10
+ // Neither honoured DOTENV_CONFIG_PATH, and they had already drifted in how
11
+ // each suppressed dotenv's banner — one conditional, one unconditional.
12
+ import { loadProcessDotenv } from "./utils/dotenvBootstrap.js";
13
+ await loadProcessDotenv();
18
14
  import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
19
15
  import { AsyncLocalStorage } from "async_hooks";
20
16
  import { EventEmitter } from "events";
@@ -665,7 +665,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
665
665
  // reported "no image attached" on multimodal calls.
666
666
  const currentContents = [];
667
667
  prependConversationMessages(currentContents, options.conversationMessages);
668
- const userParts = await buildUserPartsWithMultimodal(options.input, options.input.text, "[GoogleAIStudio:stream]");
668
+ const userParts = await buildUserPartsWithMultimodal(options.input, options.input.text, "[GoogleAIStudio:stream]", modelName);
669
669
  currentContents.push({
670
670
  role: "user",
671
671
  parts: userParts,
@@ -1220,7 +1220,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
1220
1220
  // legitimately reported "no image / PDF attached".
1221
1221
  const currentContents = [];
1222
1222
  prependConversationMessages(currentContents, options.conversationMessages);
1223
- const userParts = await buildUserPartsWithMultimodal(options.input, promptText, "[GoogleAIStudio:generate]");
1223
+ const userParts = await buildUserPartsWithMultimodal(options.input, promptText, "[GoogleAIStudio:generate]", modelName);
1224
1224
  currentContents.push({
1225
1225
  role: "user",
1226
1226
  parts: userParts,
@@ -8,7 +8,7 @@
8
8
  * This module extracts the functions that are duplicated between the two
9
9
  * providers so they can share a single implementation.
10
10
  */
11
- import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
11
+ import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry, MultimodalVideoEntry } from "../../types/index.js";
12
12
  import type { Tool, ToolExecutionGuards } from "../../types/index.js";
13
13
  /**
14
14
  * A per-turn tool execute map that deduplicates identical tool calls.
@@ -466,4 +466,26 @@ export declare function prependConversationMessages(contents: Array<{
466
466
  * summary in the text part.
467
467
  */
468
468
  export declare function appendNativeAudioParts(userParts: VertexNativePart[], audioFiles: MultimodalAudioEntry[] | undefined, logPrefix?: string): Promise<void>;
469
- export declare function buildUserPartsWithMultimodal(input: GeminiMultimodalInput | undefined, textOverride?: string, logPrefix?: string): Promise<VertexNativePart[]>;
469
+ /**
470
+ * Append video to a Gemini request as `inlineData` parts.
471
+ *
472
+ * The native request body is assembled by hand rather than taken from the AI
473
+ * SDK's `file` parts, so — exactly as with audio — a `{ type: "file" }` part
474
+ * built upstream never arrives here. Video has to be added explicitly or it
475
+ * is silently dropped on both Gemini front ends while the capability table
476
+ * says it is supported.
477
+ *
478
+ * A clip the provider will not take inline is skipped rather than sent: an
479
+ * oversized or unsupported `inlineData` part fails the whole request, whereas
480
+ * skipping leaves the caller with the metadata summary and the keyframes,
481
+ * which is what it had before native delivery existed.
482
+ *
483
+ * `model` is forwarded to `canDeliverVideoNatively` so a Claude-on-Vertex
484
+ * request — same `providerName` ("vertex") as a Gemini-on-Vertex one, routed
485
+ * to the native Anthropic SDK instead by `GoogleVertexProvider.isAnthropicModel`
486
+ * — is answered as not natively video-capable rather than inheriting Vertex's
487
+ * Gemini row. Optional because AI Studio's caller never has a Claude model to
488
+ * pass.
489
+ */
490
+ export declare function appendNativeVideoParts(userParts: VertexNativePart[], videoFiles: MultimodalVideoEntry[] | undefined, providerName: string, model?: string, logPrefix?: string): Promise<void>;
491
+ export declare function buildUserPartsWithMultimodal(input: GeminiMultimodalInput | undefined, textOverride?: string, logPrefix?: string, model?: string): Promise<VertexNativePart[]>;
@@ -13,6 +13,7 @@ import { existsSync, readFileSync } from "node:fs";
13
13
  import { extname } from "node:path";
14
14
  import { DEFAULT_CONTEXT_GUARD_RATIO, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../../core/constants.js";
15
15
  import { needsAudioTranscode, toProviderCompatibleAudio, } from "../../adapters/audioFormatSupport.js";
16
+ import { canDeliverVideoNatively } from "../../adapters/videoFormatSupport.js";
16
17
  import { logger } from "../../utils/logger.js";
17
18
  import { guardToolExecutor } from "../../core/toolExecutionGuards.js";
18
19
  import { resolveSamplingParams } from "../../models/modelRegistry.js";
@@ -1376,7 +1377,73 @@ export async function appendNativeAudioParts(userParts, audioFiles, logPrefix =
1376
1377
  logger.debug(`${logPrefix} Added native audio part for ${base} (${compatible.mimeType})`);
1377
1378
  }
1378
1379
  }
1379
- export async function buildUserPartsWithMultimodal(input, textOverride, logPrefix = "[GeminiNative]") {
1380
+ /**
1381
+ * Append video to a Gemini request as `inlineData` parts.
1382
+ *
1383
+ * The native request body is assembled by hand rather than taken from the AI
1384
+ * SDK's `file` parts, so — exactly as with audio — a `{ type: "file" }` part
1385
+ * built upstream never arrives here. Video has to be added explicitly or it
1386
+ * is silently dropped on both Gemini front ends while the capability table
1387
+ * says it is supported.
1388
+ *
1389
+ * A clip the provider will not take inline is skipped rather than sent: an
1390
+ * oversized or unsupported `inlineData` part fails the whole request, whereas
1391
+ * skipping leaves the caller with the metadata summary and the keyframes,
1392
+ * which is what it had before native delivery existed.
1393
+ *
1394
+ * `model` is forwarded to `canDeliverVideoNatively` so a Claude-on-Vertex
1395
+ * request — same `providerName` ("vertex") as a Gemini-on-Vertex one, routed
1396
+ * to the native Anthropic SDK instead by `GoogleVertexProvider.isAnthropicModel`
1397
+ * — is answered as not natively video-capable rather than inheriting Vertex's
1398
+ * Gemini row. Optional because AI Studio's caller never has a Claude model to
1399
+ * pass.
1400
+ */
1401
+ export async function appendNativeVideoParts(userParts, videoFiles, providerName, model, logPrefix = "[GeminiNative]") {
1402
+ if (!videoFiles || videoFiles.length === 0) {
1403
+ return;
1404
+ }
1405
+ // Tracks source bytes already committed to this request's shared inline
1406
+ // budget. This is the sole place video ceiling enforcement happens: the AI
1407
+ // SDK `file`-part assembly in messageBuilder.ts never delivers video for
1408
+ // either Gemini provider class (both own their request assembly end to
1409
+ // end), so a duplicate of this check used to sit there too — permanently
1410
+ // inert and unreachable by any test — and has been removed rather than
1411
+ // left as a second, silently-driftable copy of the same logic.
1412
+ //
1413
+ // Seeded from the PDF, image and audio `inlineData` parts the caller has
1414
+ // already pushed onto `userParts` before this runs, not just from sibling
1415
+ // video clips: the 20MB Gemini request ceiling is shared by every inline
1416
+ // part, so a clip that would fit alone can still overflow the request
1417
+ // once those other parts are counted. `data` is base64, which costs 4
1418
+ // bytes per 3 of source — decode the length back to source bytes so this
1419
+ // total stays comparable to `video.buffer.length` below.
1420
+ let committedVideoBytes = userParts.reduce((total, part) => {
1421
+ if ("inlineData" in part) {
1422
+ return total + Math.floor((part.inlineData.data.length * 3) / 4);
1423
+ }
1424
+ return total;
1425
+ }, 0);
1426
+ for (const video of videoFiles) {
1427
+ // Split on both separators: a Windows-style name reaching a POSIX host
1428
+ // would otherwise keep its whole path in the log line.
1429
+ const base = video.filename.split(/[\\/]/).pop() ?? video.filename;
1430
+ const decision = canDeliverVideoNatively(providerName, video, committedVideoBytes, model);
1431
+ if (!decision.deliver) {
1432
+ logger.warn(`${logPrefix} Sending keyframes instead of the clip for ${base}: ` +
1433
+ `${decision.reason}.`);
1434
+ continue;
1435
+ }
1436
+ committedVideoBytes += video.buffer.length;
1437
+ userParts.push({
1438
+ inlineData: {
1439
+ mimeType: video.mimeType,
1440
+ data: video.buffer.toString("base64"),
1441
+ },
1442
+ });
1443
+ logger.debug(`${logPrefix} Added native video part for ${base} (${video.mimeType})`);
1444
+ }
1445
+ }
1446
+ export async function buildUserPartsWithMultimodal(input, textOverride, logPrefix = "[GeminiNative]", model) {
1380
1447
  const text = typeof textOverride === "string" ? textOverride : (input?.text ?? "");
1381
1448
  const parts = [{ text }];
1382
1449
  if (input?.pdfFiles && input.pdfFiles.length > 0) {
@@ -1483,5 +1550,13 @@ export async function buildUserPartsWithMultimodal(input, textOverride, logPrefi
1483
1550
  // Vertex client left this front end advertising native audio via
1484
1551
  // NATIVE_AUDIO_PROVIDERS and then dropping the bytes on the floor.
1485
1552
  await appendNativeAudioParts(parts, input?.nativeAudioFiles, logPrefix);
1553
+ // `google-ai-studio` rather than the caller's provider string: this helper
1554
+ // is the AI Studio / Gemini 3 assembly path, and the capability table is
1555
+ // keyed by provider name. Passing a name the table does not know would make
1556
+ // every clip fall back to keyframes on the one front end that can watch it.
1557
+ // `model` is threaded through for signature parity with the Vertex call
1558
+ // site; AI Studio never hosts a Claude model, so it makes no delivery
1559
+ // difference here, only in `canDeliverVideoNatively`'s Vertex branch.
1560
+ await appendNativeVideoParts(parts, input?.nativeVideoFiles, "google-ai-studio", model, logPrefix);
1486
1561
  return parts;
1487
1562
  }
@@ -6,7 +6,7 @@ import path from "path";
6
6
  import { ErrorCategory, ErrorSeverity, } from "../../constants/enums.js";
7
7
  import { BaseProvider } from "../../core/baseProvider.js";
8
8
  import { unwrapImagePayload } from "../../adapters/imageFormatSupport.js";
9
- import { appendNativeAudioParts } from "../googleNativeGemini3/utils.js";
9
+ import { appendNativeAudioParts, appendNativeVideoParts, } from "../googleNativeGemini3/utils.js";
10
10
  import { getMimeTypeForExtension } from "../../processors/config/mimeConstants.js";
11
11
  import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, resolveToolTimeoutMs, TOOL_STORAGE_TIMEOUT_MS, } from "../../core/constants.js";
12
12
  import { resolveRequestKind } from "../../core/resolveRequestKind.js";
@@ -1302,6 +1302,11 @@ export class GoogleVertexProvider extends BaseProvider {
1302
1302
  });
1303
1303
  }
1304
1304
  }
1305
+ // Video last, after images: `appendNativeVideoParts` seeds its
1306
+ // request-wide byte budget from whatever `inlineData` parts already sit
1307
+ // in `userParts`, so the images above must be pushed before it runs or
1308
+ // their bytes are invisible to the ceiling check that follows.
1309
+ await appendNativeVideoParts(userParts, multimodalInput?.nativeVideoFiles, "vertex", modelName, "[GoogleVertex]");
1305
1310
  // Prepend prior conversation turns before the current user message so
1306
1311
  // multi-turn callers (memory, loop REPL, agent flows) actually carry
1307
1312
  // context. Without this, the native Vertex Gemini stream rebuilt the
@@ -2114,6 +2119,11 @@ export class GoogleVertexProvider extends BaseProvider {
2114
2119
  });
2115
2120
  }
2116
2121
  }
2122
+ // Video last, after images: `appendNativeVideoParts` seeds its
2123
+ // request-wide byte budget from whatever `inlineData` parts already sit
2124
+ // in `userParts`, so the images above must be pushed before it runs or
2125
+ // their bytes are invisible to the ceiling check that follows.
2126
+ await appendNativeVideoParts(userParts, multimodalInput?.nativeVideoFiles, "vertex", modelName, "[GoogleVertex]");
2117
2127
  // Prepend prior conversation turns before the current user message so
2118
2128
  // multi-turn callers (memory, loop REPL, agent flows) carry context
2119
2129
  // into native Vertex Gemini generate. Without this, every call started
@@ -62,6 +62,105 @@ export type MultimodalAudioEntry = {
62
62
  /** Detected MIME type of `buffer`. */
63
63
  mimeType: string;
64
64
  };
65
+ /**
66
+ * One video file destined for native delivery to a provider.
67
+ *
68
+ * Mirrors {@link MultimodalAudioEntry}: the bytes travel rather than the path,
69
+ * because whether a video is sent at all is decided per provider, after
70
+ * detection has already read the file.
71
+ *
72
+ * `durationSec` rides along because the native-delivery gate is expressed in
73
+ * seconds as well as bytes, and re-probing the container at dispatch time
74
+ * would mean a second ffprobe run for something the processor already
75
+ * measured. It is optional: probing can fail (no ffmpeg, an exotic container),
76
+ * and an unknown duration must not by itself disqualify a clip that is
77
+ * comfortably under the size ceiling.
78
+ */
79
+ export type MultimodalVideoEntry = {
80
+ /** Raw video bytes, as detected. */
81
+ buffer: Buffer;
82
+ /** Display name; may be a full path, so log only its basename. */
83
+ filename: string;
84
+ /** Detected MIME type of `buffer`. */
85
+ mimeType: string;
86
+ /** Clip length in seconds, when the processor was able to measure it. */
87
+ durationSec?: number;
88
+ };
89
+ /**
90
+ * How one provider handles an attached video.
91
+ *
92
+ * The table lives in `adapters/videoFormatSupport.ts`; this is its row shape.
93
+ * `apiType` names the mechanism that is actually implemented, not the one a
94
+ * provider theoretically offers — Gemini also exposes a resumable Files API
95
+ * for clips beyond the inline ceiling, and until that is wired up calling this
96
+ * row "files-api" would misdescribe what happens to a 200 MB upload.
97
+ */
98
+ export type VideoProviderConfig = {
99
+ /** Whether raw video bytes can be handed to this provider at all. */
100
+ readonly supportsNativeVideo: boolean;
101
+ /** How the video reaches the model. */
102
+ readonly apiType: "inline" | "frame-extraction";
103
+ /**
104
+ * Inline source-byte budget for the whole request, in MB. A clip is
105
+ * delivered natively only while its bytes plus every inline part already in
106
+ * the request stay within it; otherwise it falls back to keyframes.
107
+ * Meaningless when `supportsNativeVideo` is false, and set to 0 there rather
108
+ * than to a number that reads like a real limit.
109
+ */
110
+ readonly maxSizeMB: number;
111
+ /** Longest clip accepted natively, in seconds. 0 when not applicable. */
112
+ readonly maxDurationSec: number;
113
+ /** Whether the provider hears the video's audio track as well as seeing it. */
114
+ readonly supportsAudio: boolean;
115
+ /**
116
+ * Keyframe budget to aim for when this provider gets frames instead of the
117
+ * video. Advisory: an explicit `videoOptions.frames` always wins.
118
+ */
119
+ readonly recommendedFrameCount: number;
120
+ };
121
+ /**
122
+ * Media collected during file detection that a provider may be able to
123
+ * consume directly, rather than as a text summary.
124
+ *
125
+ * Grouped rather than passed as two more positional parameters: the message
126
+ * converters already take text, images, PDFs, provider and model, and each
127
+ * new modality added one more argument to a call nobody could read. A bag
128
+ * also means the next modality is a field, not another signature change at
129
+ * every call site.
130
+ */
131
+ export type NativeMediaAttachments = {
132
+ readonly audio?: MultimodalAudioEntry[];
133
+ readonly video?: MultimodalVideoEntry[];
134
+ /**
135
+ * Transcode target for an incompatible image (see
136
+ * `GenerateOptions.imageOptions`). Not itself a native attachment, but
137
+ * bundled here for the same reason audio and video are: it is one more
138
+ * per-request option the message converters need at the same call sites.
139
+ */
140
+ readonly outputFormat?: VisionImageOutputFormat;
141
+ };
142
+ /**
143
+ * Outcome of asking whether one video may be handed to one provider as bytes.
144
+ *
145
+ * A plain boolean collapsed "this provider never watches video" with "this
146
+ * provider would have, but the clip is 400 MB" — and the two want different
147
+ * log lines and different advice. `reason` is populated exactly when
148
+ * `deliver` is false, and is phrased for a user to read.
149
+ *
150
+ * The accepting arm declares `reason?: undefined` rather than omitting the
151
+ * field. Not decoration: `build:react-hooks` type-checks this graph without
152
+ * `--strict`, and there a negated boolean-literal discriminant does not
153
+ * narrow, so `decision.reason` inside `if (!decision.deliver)` fails to
154
+ * compile unless the property exists on both arms. Declaring it keeps the
155
+ * union exact under strict and compilable under both.
156
+ */
157
+ export type VideoDeliveryDecision = {
158
+ readonly deliver: true;
159
+ readonly reason?: undefined;
160
+ } | {
161
+ readonly deliver: false;
162
+ readonly reason: string;
163
+ };
65
164
  /**
66
165
  * Broad category a file format belongs to, as a human would name it.
67
166
  *
@@ -190,6 +289,14 @@ export type FileProcessingResult = {
190
289
  hasImages?: boolean;
191
290
  frameCount?: number;
192
291
  hasKeyframes?: boolean;
292
+ /**
293
+ * Clip length in seconds, when the processor could measure it.
294
+ *
295
+ * Surfaced here so the native-delivery gate does not have to re-probe a
296
+ * container the processor has already opened. Absent when probing failed
297
+ * — an unknown duration, not a zero-length clip.
298
+ */
299
+ durationSec?: number;
193
300
  };
194
301
  };
195
302
  /**
@@ -17,7 +17,7 @@ import type { AvatarOptions, AvatarResult } from "./avatar.js";
17
17
  import type { MusicOptions, MusicResult } from "./music.js";
18
18
  import type { StandardRecord, ValidationSchema, ZodUnknownSchema } from "./aliases.js";
19
19
  import type { NeurolinkCredentials } from "./providers.js";
20
- import type { CSVProcessorOptions, FileWithMetadata, MultimodalAudioEntry, VisionImageOutputFormat } from "./file.js";
20
+ import type { CSVProcessorOptions, FileWithMetadata, MultimodalAudioEntry, MultimodalVideoEntry, VisionImageOutputFormat } from "./file.js";
21
21
  import type { WorkflowConfig } from "./workflow.js";
22
22
  import type { Schema, Tool, ToolChoice } from "./tools.js";
23
23
  import type { StepResult, LanguageModel } from "./providers.js";
@@ -68,6 +68,17 @@ export type GenerateOptions = {
68
68
  */
69
69
  nativeAudioFiles?: MultimodalAudioEntry[];
70
70
  videoFiles?: Array<Buffer | string>;
71
+ /**
72
+ * Video collected during file detection, carried through to a provider
73
+ * that can watch it. Populated by detection rather than by callers.
74
+ *
75
+ * Separate from `videoFiles` above, which is the caller-facing input that
76
+ * yields a metadata summary plus extracted keyframes. This one carries the
77
+ * decoded bytes forward so a provider that accepts video receives the clip
78
+ * itself; providers that cannot fall back to the frames and this is
79
+ * ignored.
80
+ */
81
+ nativeVideoFiles?: MultimodalVideoEntry[];
71
82
  files?: Array<Buffer | string | FileWithMetadata>;
72
83
  content?: Content[];
73
84
  /**
@@ -7,7 +7,7 @@ import { AIProviderName, AnthropicModels, BedrockModels, DeepSeekModels, GoogleA
7
7
  import type { ValidationSchema } from "./aliases.js";
8
8
  import type { DecisionLimits, DecisionRequest, DecisionResult } from "./decision.js";
9
9
  import type { EnhancedGenerateResult, TextGenerationOptions } from "./generate.js";
10
- import type { MultimodalAudioEntry } from "./file.js";
10
+ import type { MultimodalAudioEntry, MultimodalVideoEntry } from "./file.js";
11
11
  import type { StreamOptions, StreamResult } from "./stream.js";
12
12
  import type { ProviderError, ProviderErrorRule } from "./errors.js";
13
13
  import type { ExternalMCPToolInfo } from "./externalMcp.js";
@@ -2206,6 +2206,13 @@ export type GeminiMultimodalInput = {
2206
2206
  * are already-materialised bytes with a resolved mime type.
2207
2207
  */
2208
2208
  nativeAudioFiles?: MultimodalAudioEntry[];
2209
+ /**
2210
+ * Video collected during file detection, carried through to the native
2211
+ * request as `inlineData`. Distinct from the user-facing `videoFiles`: these
2212
+ * are already-materialised bytes with a resolved mime type and, where it
2213
+ * could be measured, the clip's duration.
2214
+ */
2215
+ nativeVideoFiles?: MultimodalVideoEntry[];
2209
2216
  };
2210
2217
  /**
2211
2218
  * Internal helpers used by the conversation-history builder in
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Implicit `.env` bootstrap, shared by the SDK entry point and the CLI.
3
+ *
4
+ * Both sites used to call dotenv's `config()` with no options beyond
5
+ * `quiet`, and a direct `config()` call does NOT read `DOTENV_CONFIG_PATH`
6
+ * — only dotenv's own `dotenv/config` preload entry does. So the strip this
7
+ * repo documents and uses to vet suites,
8
+ *
9
+ * env -i HOME=… PATH=… CI=true DOTENV_CONFIG_PATH=/dev/null …
10
+ *
11
+ * stripped nothing the moment a suite imported the built SDK: importing
12
+ * `dist/index.js` loads `src/lib/neurolink.ts`, which loaded `.env` from the
13
+ * working directory regardless. A credential the strip was supposed to
14
+ * remove was present at every read site, and a suite that only passes
15
+ * because of it looks identical to one that does not need it. See #1744;
16
+ * the CI-only failure in #1735 is what surfaced it.
17
+ *
18
+ * Honouring the variable here makes that strip real. It is additive: with
19
+ * `DOTENV_CONFIG_PATH` unset — which is every existing caller, SDK consumers
20
+ * included — this does exactly what it did before, loading `.env` from the
21
+ * current working directory. Only a caller that explicitly sets the variable
22
+ * sees a difference, and what it then sees is dotenv's own documented
23
+ * meaning for it.
24
+ */
25
+ export declare function loadProcessDotenv(): Promise<void>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Implicit `.env` bootstrap, shared by the SDK entry point and the CLI.
3
+ *
4
+ * Both sites used to call dotenv's `config()` with no options beyond
5
+ * `quiet`, and a direct `config()` call does NOT read `DOTENV_CONFIG_PATH`
6
+ * — only dotenv's own `dotenv/config` preload entry does. So the strip this
7
+ * repo documents and uses to vet suites,
8
+ *
9
+ * env -i HOME=… PATH=… CI=true DOTENV_CONFIG_PATH=/dev/null …
10
+ *
11
+ * stripped nothing the moment a suite imported the built SDK: importing
12
+ * `dist/index.js` loads `src/lib/neurolink.ts`, which loaded `.env` from the
13
+ * working directory regardless. A credential the strip was supposed to
14
+ * remove was present at every read site, and a suite that only passes
15
+ * because of it looks identical to one that does not need it. See #1744;
16
+ * the CI-only failure in #1735 is what surfaced it.
17
+ *
18
+ * Honouring the variable here makes that strip real. It is additive: with
19
+ * `DOTENV_CONFIG_PATH` unset — which is every existing caller, SDK consumers
20
+ * included — this does exactly what it did before, loading `.env` from the
21
+ * current working directory. Only a caller that explicitly sets the variable
22
+ * sees a difference, and what it then sees is dotenv's own documented
23
+ * meaning for it.
24
+ */
25
+ export async function loadProcessDotenv() {
26
+ try {
27
+ // Suppress dotenv v17's stdout banner — it pollutes CLI JSON output.
28
+ // Redundant next to the explicit `quiet` below, which is what actually
29
+ // silences a direct `config()` call; kept because both call sites set it
30
+ // and something downstream may still read it.
31
+ process.env.DOTENV_CONFIG_QUIET = process.env.DOTENV_CONFIG_QUIET ?? "true";
32
+ const { config } = await import("dotenv");
33
+ const path = process.env.DOTENV_CONFIG_PATH;
34
+ // Pass `path` only when set: `{ path: undefined }` is not the same as
35
+ // omitting it for every dotenv version, and the default must stay
36
+ // byte-for-byte what it was.
37
+ config(path ? { quiet: true, path } : { quiet: true });
38
+ }
39
+ catch {
40
+ // dotenv ships as a normal runtime dependency (see package.json), so this
41
+ // branch should not be reachable in a standard install. It stays as a
42
+ // defensive fallback for an install that has pruned or bundled
43
+ // dependencies unusually, so a missing module degrades to "no .env
44
+ // loaded" instead of crashing the SDK/CLI entry point.
45
+ }
46
+ }
@@ -1132,6 +1132,14 @@ export class FileDetector {
1132
1132
  ...detection.metadata,
1133
1133
  frameCount: videoResult.data.frameCount,
1134
1134
  hasKeyframes: videoResult.data.hasKeyframes,
1135
+ // Carried so native delivery can apply its duration ceiling
1136
+ // without opening the container a second time. The processor
1137
+ // reports 0 when nothing could probe it; that is "unknown", not a
1138
+ // zero-length clip, so it is left off rather than passed through
1139
+ // as a number a ceiling check would happily accept.
1140
+ ...(videoResult.data.metadata.duration > 0
1141
+ ? { durationSec: videoResult.data.metadata.duration }
1142
+ : {}),
1135
1143
  },
1136
1144
  };
1137
1145
  }
@@ -16,6 +16,7 @@ import { ErrorFactory, NeuroLinkError, withTimeout } from "./errorHandling.js";
16
16
  import { FileDetector } from "./fileDetector.js";
17
17
  import { detectIsoBmffImageMimeType } from "./isoBmff.js";
18
18
  import { needsAudioTranscode, supportsNativeAudio, toProviderCompatibleAudio, } from "../adapters/audioFormatSupport.js";
19
+ import { supportsNativeVideo } from "../adapters/videoFormatSupport.js";
19
20
  import { getImageCache } from "./imageCache.js";
20
21
  import { ImageProcessor, imageUtils } from "./imageProcessor.js";
21
22
  import { logger } from "./logger.js";
@@ -697,6 +698,26 @@ async function appendDetectedFileResult(result, file, options) {
697
698
  ];
698
699
  logger.info(`[FileDetector] Added ${result.images.length} video keyframes as images`);
699
700
  }
701
+ // Carry the bytes forward alongside the summary and the frames, the same
702
+ // way audio does. Whether the clip is sent is decided later, per provider:
703
+ // one that can watch receives it, one that cannot still gets the keyframes
704
+ // and is no worse off than before. This matters most where the frames are
705
+ // absent — ffmpeg is optional, and without it the summary was everything
706
+ // the model saw.
707
+ const videoBytes = await readFileInputBytes(file);
708
+ if (videoBytes) {
709
+ options.input.nativeVideoFiles = [
710
+ ...(options.input.nativeVideoFiles || []),
711
+ {
712
+ buffer: videoBytes,
713
+ filename,
714
+ mimeType: result.mimeType,
715
+ ...(typeof result.metadata?.durationSec === "number"
716
+ ? { durationSec: result.metadata.durationSec }
717
+ : {}),
718
+ },
719
+ ];
720
+ }
700
721
  logger.info(`[FileDetector] ✅ Video: ${filename}`);
701
722
  }
702
723
  else if (result.type === "audio") {
@@ -1376,8 +1397,14 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1376
1397
  // summary survives.
1377
1398
  const hasNativeAudio = (inp.nativeAudioFiles?.length ?? 0) > 0 &&
1378
1399
  supportsNativeAudio(provider, model);
1379
- // If no images, PDFs or audio, use standard message building and convert to MultimodalChatMessage[]
1380
- if (!hasImages && !hasPDFs && !hasNativeAudio) {
1400
+ // Video that is to be delivered natively is multimodal for exactly the
1401
+ // reason audio is: it becomes a non-text part. Gated on the provider
1402
+ // accepting video so one that only ever sees keyframes keeps the cheaper
1403
+ // shape it already had.
1404
+ const hasNativeVideo = (inp.nativeVideoFiles?.length ?? 0) > 0 &&
1405
+ supportsNativeVideo(provider, model);
1406
+ // If no images, PDFs, audio or video, use standard message building and convert to MultimodalChatMessage[]
1407
+ if (!hasImages && !hasPDFs && !hasNativeAudio && !hasNativeVideo) {
1381
1408
  // #289: CSV content[] items don't need vision, so they never reach the
1382
1409
  // multimodal converter below — process them into the prompt text here
1383
1410
  // (otherwise a `content: [{type:"csv"}]`-only request silently drops it).
@@ -1495,7 +1522,11 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1495
1522
  // audio-only turn fell through to the plain-text branch below, so the
1496
1523
  // recording was dropped and only its metadata summary — already folded
1497
1524
  // into `text` — ever reached the model.
1498
- (inp.nativeAudioFiles?.length ?? 0) > 0) {
1525
+ (inp.nativeAudioFiles?.length ?? 0) > 0 ||
1526
+ // Same for a video-only turn. On a machine without ffmpeg there are no
1527
+ // keyframes to put in `images`, so this clause is the only thing keeping
1528
+ // the clip out of the plain-text branch.
1529
+ (inp.nativeVideoFiles?.length ?? 0) > 0) {
1499
1530
  userContent = await convertMultimodalToProviderFormat(inp.text ?? "", inp.images || [], pdfFiles, provider, model, {
1500
1531
  audioFiles: inp.nativeAudioFiles ?? [],
1501
1532
  outputFormat: options.imageOptions?.outputFormat,
@@ -1597,6 +1628,13 @@ async function convertContentToProviderFormat(content, provider, model, pdfOptio
1597
1628
  // change exists to stop, reached through a different branch. Gated on the
1598
1629
  // provider accepting audio so one that cannot keeps the cheaper plain-text
1599
1630
  // shape rather than an array carrying a part it will ignore.
1631
+ //
1632
+ // Video does not get the same treatment here: native delivery for the two
1633
+ // Gemini provider classes never reaches this function with any consequence
1634
+ // (both fully own their own request assembly — see the module comment on
1635
+ // `appendNativeVideoParts` in googleNativeGemini3/utils.ts, the actual and
1636
+ // only place a video clip is attached), so a `deliversVideo` flag here would
1637
+ // gate nothing but a value nobody reads.
1600
1638
  const deliversAudio = audioFiles.length > 0 && supportsNativeAudio(provider, model);
1601
1639
  const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0 || deliversAudio;
1602
1640
  // Validate that we have at least some content
@@ -2252,6 +2290,22 @@ mediaOptions = {}) {
2252
2290
  `${compatible.converted ? ` (converted to ${compatible.mimeType})` : ""}`);
2253
2291
  }
2254
2292
  }
2293
+ // Video is deliberately not attached here. This function's output — an AI
2294
+ // SDK `content` array — only ever reaches a real request for providers that
2295
+ // call `BaseProvider`'s shared message building unmodified, and neither
2296
+ // Gemini provider class does: `GoogleAIStudioProvider.generate()` and
2297
+ // `GoogleVertexProvider.generate()` are full overrides that never call
2298
+ // `super.generate()`, and `BaseProvider.stream()`'s early multimodal check
2299
+ // builds a `messages` value only to test `hasVideoFrames()` on it before
2300
+ // discarding it — so a video part built here never travels anywhere. The
2301
+ // one delivery mechanism both Gemini front ends actually use is the
2302
+ // hand-built `inlineData` request in `appendNativeVideoParts`
2303
+ // (googleNativeGemini3/utils.ts), which carries its own aggregate-ceiling
2304
+ // enforcement. An earlier version of this function duplicated that
2305
+ // ceiling check here too, permanently inert and with no test able to
2306
+ // reach it — that duplicate has been removed rather than left as a second
2307
+ // place the same bug could be introduced without either delivery or a
2308
+ // failing test ever showing it.
2255
2309
  // Check if provider supports native PDF processing
2256
2310
  const supportsNativePDF = PDFProcessor.supportsNativePDF(provider);
2257
2311
  if (supportsNativePDF) {