@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
544 lines (543 loc) • 27.5 kB
TypeScript
/**
* Shared utilities for Gemini 3 native SDK support.
*
* Both GoogleAIStudioProvider and GoogleVertexProvider route Gemini 3 models
* with tools to the native @google/genai SDK (bypassing the Vercel AI SDK)
* in order to properly handle thought_signature in multi-turn tool calling.
*
* This module extracts the functions that are duplicated between the two
* providers so they can share a single implementation.
*/
import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry, MultimodalVideoEntry, ModelMessage } from "../../types/index.js";
import type { Tool, ToolExecutionGuards } from "../../types/index.js";
/**
* A per-turn tool execute map that deduplicates identical tool calls.
*
* Gemini occasionally re-emits a tool call with identical arguments across
* agentic steps even though the prior result is already in the conversation
* history (BZ-3327). Re-executing produces duplicate side effects and
* duplicate reports for the user, plus wasted tokens. This map caches the
* result of each {tool name + args} executed within the turn and returns it
* for any identical re-request instead of running the tool again.
*
* Providers build a fresh executeMap per request, so the cache scope is
* exactly one turn. Only `.get()` is overridden (the sole access path used by
* the native agentic loops), so iteration still yields the raw executors.
*/
export declare class DedupExecuteMap extends Map<string, Tool["execute"]> {
private readonly resultCache;
get(name: string): Tool["execute"] | undefined;
}
/**
* Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
*
* This handles both Zod schemas and plain JSON Schema objects for tool parameters.
*/
export declare function buildNativeToolDeclarations(tools: Record<string, Tool>, reservedNames?: ReadonlySet<string>): NativeToolDeclarationsResult;
/**
* Build the tool record handed to `runAgenticLoop`, routed through the turn's
* DedupExecuteMap.
*
* The engine looks tools up by the name the adapter reports, which is the
* ORIGINAL caller-facing name; `executeMap` is keyed by the SANITIZED wire
* name Google actually declares. `originalNameMap` is the bridge, and it
* carries an entry for every converted tool (identity mappings included), so
* iterating it yields exactly the declared, executable set.
*
* Going through `executeMap.get()` rather than the raw `tool.execute` is the
* entire point: `.get()` returns the dedup wrapper, so an identical
* {name, args} repeated within one turn is answered from the per-turn cache
* instead of running the tool again (BZ-3327). Passing the raw executor looks
* identical in every test that calls a tool once, and silently reintroduces
* duplicate side effects the moment the model repeats itself.
*/
export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined, guards?: ToolExecutionGuards): NonNullable<AgenticLoopOptions["tools"]>;
/**
* Mid-turn tool sync for the native Gemini loops that build their snapshot
* via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
* discovered tools into the live record between steps; without this refresh
* they stay invisible to the rest of the turn and every call dies as
* TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
* `toolsConfig` by reference — and returns the original names of the tools it
* added (empty when nothing was added).
*/
export declare function refreshNativeToolDeclarations(liveTools: Record<string, Tool> | undefined, current: NativeToolDeclarationsResult): string[];
/**
* Build the native @google/genai config object shared by stream and generate.
*
* Caller is responsible for the tools-vs-JSON conflict resolution: Gemini's
* function calling cannot be combined with `responseMimeType:
* "application/json"`, and `responseSchema` requires that mime type. So
* when tools are active, callers must NOT pass `wantsJsonOutput`/
* `responseSchema` here; when JSON/schema output is requested, callers
* must omit `toolsConfig`. The AI Studio path enforces this by forcing
* `disableTools: true` whenever JSON/schema output is requested.
*/
export declare function buildNativeConfig(options: {
temperature?: number;
/**
* Nucleus sampling. Subject to the same registry-driven
* sampling-param strip as `temperature` — a model that rejects
* classic sampling params (see `SAMPLING_PARAM_REJECTING_FAMILIES`)
* gets neither on the wire.
*/
topP?: number;
maxTokens?: number;
systemPrompt?: string;
/**
* Model id, used for the registry-driven sampling-param strip and to
* pick the right thinkingConfig wire shape (thinkingLevel for Gemini 3,
* a translated numeric thinkingBudget for Gemini 2.5).
*/
model?: string;
thinkingConfig?: ThinkingConfig;
/**
* When true (and `toolsConfig` is undefined), set
* `responseMimeType: "application/json"` to enforce native JSON output.
*/
wantsJsonOutput?: boolean;
/**
* Pre-converted JSON Schema for native `responseSchema`. Implies
* `wantsJsonOutput`. Ignored if `toolsConfig` is present.
*/
responseSchema?: Record<string, unknown>;
}, toolsConfig?: NativeToolsConfig): Record<string, unknown>;
/**
* Compute a safe, clamped maxSteps value.
*/
export declare function computeMaxSteps(rawMaxSteps?: number): number;
/**
* Map a `@google/genai` `Candidate.finishReason` enum value onto NeuroLink's
* unified finish reason, mirroring anthropic.ts `mapAnthropicStopReason`.
*
* Enum values per `@google/genai` `FinishReason`: STOP, MAX_TOKENS, SAFETY,
* RECITATION, LANGUAGE, OTHER, BLOCKLIST, PROHIBITED_CONTENT, SPII,
* MALFORMED_FUNCTION_CALL, IMAGE_SAFETY, UNEXPECTED_TOOL_CALL,
* FINISH_REASON_UNSPECIFIED. Unknown / unset / non-terminal values default to
* "stop" (a clean completion is the safe assumption).
*
* Returns a plain string (not a `{ unified, raw }` object): the Vertex result
* builders and the consuming layer (neurolink.ts `finishReason || "unknown"`
* and `finishReason === "length"`) compare against plain strings.
*
* MALFORMED_FUNCTION_CALL / UNEXPECTED_TOOL_CALL map to "error", NOT
* "tool-calls": they are provider/model failures, while "tool-calls" is the
* exclusive contract for "step budget exhausted while the model still wanted
* tools" — consumers (e.g. curator's step-cap intercept) branch on it and
* were rendering fake step-limit messages for 2-4-step malformed-call turns.
*/
export declare function mapGeminiFinishReason(raw: string | null | undefined): "stop" | "length" | "tool-calls" | "content-filter" | "error";
/**
* Append a step's text to a running cross-step accumulator, ignoring empty
* steps and inserting a single newline between non-empty contributions.
*
* The native Gemini loops overwrite per-step text into `lastStepText`, so when
* the loop is force-terminated by the step cap the intermediate tool-step prose
* is lost and a canned placeholder becomes the answer. Accumulating here mirrors
* the Vertex-Claude loop's `aggregatedTurnText += block.text` so the gathered
* text can be surfaced at the maxSteps-exhaustion exit instead of the placeholder.
*/
export declare function appendStepText(accumulated: string, stepText: string): string;
/**
* Process stream chunks to extract raw response parts, function calls, and usage metadata.
*
* Consumes the full async iterable and returns all collected data.
*/
export declare function collectStreamChunks(stream: AsyncIterable<{
functionCalls?: NativeFunctionCall[];
[key: string]: unknown;
}>): Promise<CollectedChunkResult>;
/**
* Iterate a single stream step incrementally, pushing text parts to `channel`
* as they arrive from the network while simultaneously accumulating the full
* `CollectedChunkResult` needed for history and token accounting.
*
* Used for all steps (both intermediate tool-calling steps and the final
* text-only step). Text parts are pushed to the channel as they arrive,
* enabling truly incremental streaming. The complete `rawResponseParts`
* (including thoughtSignature) are still returned at the end for use by
* `pushModelResponseToHistory`.
*/
export declare function collectStreamChunksIncremental(stream: AsyncIterable<{
functionCalls?: NativeFunctionCall[];
[key: string]: unknown;
}>, channel: StreamChannel<{
content: string;
}>): Promise<CollectedChunkResult>;
/**
* Extract the thoughtSignature token from raw response parts.
* Returns the last thoughtSignature found (each step may produce one).
*/
export declare function extractThoughtSignature(rawResponseParts: unknown[]): string | undefined;
/**
* Extract text from raw response parts, filtering out non-text parts
* (thoughtSignature, functionCall) to avoid SDK warnings.
*/
export declare function extractTextFromParts(rawResponseParts: unknown[]): string;
/**
* Execute a batch of native function calls with retry tracking and permanent failure detection.
*
* @param logLabel - Label for log messages (e.g. "[GoogleAIStudio]" or "[GoogleVertex]")
* @param stepFunctionCalls - The function calls from the model
* @param executeMap - Map of tool name to execute function
* @param failedTools - Mutable map tracking per-tool failure counts
* @param allToolCalls - Mutable array accumulating all tool call records
* @param options - Optional settings for execution tracking and cancellation,
* plus an `originalNameMap` (Google-safe → consumer-supplied
* identifier) so the sanitization stays transport-only and
* consumers see the names they registered.
* @returns Array of function responses for conversation history
*/
export declare function executeNativeToolCalls(logLabel: string, stepFunctionCalls: NativeFunctionCall[], executeMap: Map<string, Tool["execute"]>, failedTools: Map<string, {
count: number;
lastError: string;
}>, allToolCalls: Array<{
toolName: string;
args: Record<string, unknown>;
}>, options?: {
toolExecutions?: Array<{
name: string;
input: Record<string, unknown>;
output: unknown;
}>;
abortSignal?: AbortSignal;
originalNameMap?: Map<string, string>;
/**
* Live tool record + declaration snapshot for mid-turn discovery: on a
* dispatch miss the executor re-resolves against `liveTools` (hydrated
* by search_tools, or auto-hydrated from the deferred catalog) and syncs
* `declarations` in place. `declarations.executeMap` must be the same
* map passed as the `executeMap` argument (true at every call site).
*/
liveTools?: Record<string, Tool>;
declarations?: NativeToolDeclarationsResult;
}): Promise<NativeFunctionResponse[]>;
/**
* Handle maxSteps termination by producing a final answer when the model was
* still calling tools as the step limit was reached.
*
* Resolution order (always returns a non-empty string): the caller's
* `finalText` if present; else the `lastStepText` gathered from the final step;
* else a graceful, user-facing cap message from {@link buildToolLoopCapMessage}
* — which replaced the legacy bracketed step-limit placeholder string.
*
* @param logLabel - Label for log messages (e.g. "[GoogleAIStudio]" or "[GoogleVertex]")
* @returns The resolved final answer text (never the legacy placeholder).
*/
export declare function handleMaxStepsTermination(logLabel: string, step: number, maxSteps: number, finalText: string, lastStepText: string): string;
/**
* Detect whether an error represents an abort/cancellation (from an
* AbortSignal firing during a fetch/stream drain). Used by the native
* Gemini-3 agentic loops to break gracefully rather than re-throw.
*/
export declare function isAbortError(error: unknown): boolean;
/**
* Build a graceful, user-facing message for when a single agentic turn hits
* the step cap without producing a final answer. Replaces the legacy
* bracketed "Tool execution limit reached" placeholder.
*
* Emit this ONLY when `step >= maxSteps` genuinely terminated the loop —
* aborted/timed-out/stalled turns must use {@link buildAbortedTurnMessage},
* {@link buildTurnTimeoutMessage}, or {@link buildTurnStalledMessage}, never
* this text (a killed 23-step turn once claimed "reached the 200-step limit").
*/
export declare function buildToolLoopCapMessage(maxSteps: number, toolCallCount: number): string;
/**
* Honest message for a turn stopped by the in-loop context guard
* (stopReason "context-cap") without a synthesized answer. Sibling of
* {@link buildToolLoopCapMessage} — context exits must never claim a step
* limit was reached.
*/
export declare function buildContextCapMessage(toolCallCount: number): string;
/**
* Honest message for a turn ended by the `turnTimeoutMs` wall-clock deadline
* (stopReason "time-limit"). Sibling of {@link buildToolLoopCapMessage} —
* time exits must never claim a step limit was reached.
*/
export declare function buildTurnTimeoutMessage(elapsedMs: number, toolCallCount: number): string;
/**
* Honest message for a turn ended by the `stallTimeoutMs` no-progress
* watchdog (stopReason "stalled") — typically a wedged tool or a hung
* model call.
*/
export declare function buildTurnStalledMessage(stallTimeoutMs: number, toolCallCount: number): string;
/**
* Honest message for a caller-aborted turn (admin kill, coding-task
* short-circuit — stopReason "aborted").
*/
export declare function buildAbortedTurnMessage(toolCallCount: number): string;
/**
* Pick the honest terminal message for a native-loop exit by its ACTUAL
* cause. The step-cap text is the fallback for genuine budget exhaustion
* only — time/stall/abort exits must never claim a step limit was reached
* (the 2026-07-03 "reached the 200-step limit" incident was a wall-clock
* abort wearing the cap message).
*
* Takes plain booleans rather than a turn clock so a loop whose wall-clock
* deadline is owned by something other than `createTurnClock` — AI Studio's
* pre-existing per-request timeout controller — can report through the same
* builders instead of growing a second set.
*/
export declare function buildLoopExitMessage(params: {
timedOut: boolean;
stalled: boolean;
/** Turn duration so far, for the time-limit message. */
elapsedMs: number;
wasAborted: boolean;
stallTimeoutMs?: number;
maxSteps: number;
toolCallCount: number;
}): string;
/**
* Wrap-up nudge injected when the remaining turn time drops inside the
* `wrapupTimeLeadMs` window — the time-budget twin of the soft step-budget
* nudge. Rides as a trailing text block on the tool-result user turn.
*/
export declare function buildWrapupNudgeText(useFinalResultTool: boolean): string;
/**
* Resolve the turn's `stopReason` discriminator from the loop's exit
* bookkeeping. Precedence: time conditions beat the generic abort flag
* (the deadline/stall watchdogs abort through the same internal controller),
* abort beats step-cap, and a provider-error finishReason (e.g. persistent
* MALFORMED_FUNCTION_CALL) beats "completed".
*/
export declare function resolveTurnStopReason(params: {
timedOut: boolean;
stalled: boolean;
wasAborted: boolean;
/** Step budget ran out AND no clean/forced answer was produced. */
cappedWithoutAnswer: boolean;
/** Context guard stopped the loop AND no clean/forced answer was produced. */
contextCappedWithoutAnswer?: boolean;
/** The turn's resolved unified finishReason. */
finishReason?: string;
}): GenerateStopReason;
/**
* Wall-clock + progress watchdogs for a native agentic turn.
*
* - Deadline: `turnTimeoutMs` (caller knob) or `defaultTurnTimeoutMs` (the
* loop's pre-existing defensive bound) arms a whole-turn timer.
* - Stall: when `stallTimeoutMs` is set, a low-frequency interval checks the
* time since the last recorded progress (chunk received, tool started or
* finished, step started).
*
* Both fire `onDeadline(kind)` exactly once each and latch a flag the loop's
* terminal handling reads to pick the honest exit message and `stopReason`.
* Timers are unref'd so they never hold the process open; call `dispose()`
* in the loop's finally.
*/
export declare function createTurnClock(params: {
/** Explicit whole-turn deadline (ms); wins over defaultTurnTimeoutMs. */
turnTimeoutMs?: number;
/** Loop-specific defensive default when turnTimeoutMs is unset (undefined = no deadline). */
defaultTurnTimeoutMs?: number;
/** No-progress watchdog (ms); undefined = disabled. */
stallTimeoutMs?: number;
/** Wrap-up lead (ms); only honored when turnTimeoutMs is explicitly set. */
wrapupTimeLeadMs?: number;
onDeadline: (kind: "timeout" | "stall") => void;
}): {
readonly timedOut: boolean;
readonly stalled: boolean;
/** True when the turn ended on a time condition (deadline or stall). */
readonly expired: boolean;
/** The effective whole-turn deadline in ms (explicit or defensive default). */
readonly turnTimeoutMs: number | undefined;
elapsedMs(): number;
/** Record progress: chunk received, tool started/finished, step started. */
noteProgress(): void;
/** True when an explicit deadline exists and remaining time is inside the wrap-up lead. */
shouldNudgeWrapup(): boolean;
dispose(): void;
};
/**
* In-loop context guard for native agentic turns.
*
* The provider reports the ACTUAL prompt size of every model call
* (usage.input_tokens + cache reads/writes on Anthropic;
* usageMetadata.promptTokenCount on Gemini). The guard tracks that number
* plus an estimate of what the current step appends (assistant output, tool
* results), and tells the loop to stop calling tools once the projected next
* prompt crosses `thresholdRatio` of the model's context window — the turn
* then synthesizes a final answer from what it has instead of stepping into
* a provider 400 ("prompt is too long") that destroys all completed work.
*
* Fail-open: until the first usage report arrives, `shouldStop()` is false.
*/
export declare function createContextGuard(contextWindowTokens: number, thresholdRatio?: number): {
/** Tokens at which the guard trips (ratio × window). */
readonly thresholdTokens: number;
/** Last observed prompt size plus estimated growth since. */
readonly projectedNextPromptTokens: number;
/**
* Record a model call's reported usage. `promptTokens` must be the FULL
* prompt size (uncached input + cache read + cache creation for
* Anthropic). The response's own output is counted as growth — it is
* appended to the conversation for the next call.
*/
noteUsage(promptTokens: number, outputTokens: number): void;
/**
* Add growth for content appended since the last model call (tool
* results, nudge text) using the ~4 chars/token heuristic.
*/
noteAppendedChars(chars: number): void;
/**
* Clear the projection after the caller has reclaimed context.
*
* The observed prompt size reflects the pre-reclaim conversation, so
* leaving it in place would keep `shouldStop()` true forever and defeat
* the reclaim. Resetting to the fail-open state means the guard stays
* quiet until the next real usage report re-establishes the truth.
*/
resetAfterReclaim(): void;
/** True when issuing another model call risks crossing the threshold. */
shouldStop(): boolean;
};
/**
* Push model response parts to conversation history, preserving thoughtSignature
* for Gemini 3 multi-turn tool calling.
*/
export declare function pushModelResponseToHistory(currentContents: Array<{
role: string;
parts: unknown[];
}>, rawResponseParts: unknown[], stepFunctionCalls: NativeFunctionCall[]): void;
/**
* Convert a Zod schema (or AI SDK `jsonSchema()` wrapper) into the shape
* `@google/genai` accepts as `responseSchema`. Mirrors the inline pipeline
* the Vertex Gemini paths already use:
*
* convertZodToJsonSchema → inlineJsonSchema → strip `$schema` → ensure
* every nested schema has a `type` (Vertex/Gemini reject schemas missing
* that field, even on nested objects).
*
* Lives here so the AI Studio and Vertex paths can share the same
* sanitization without duplicating the schema-conversion churn.
*/
export declare function buildGeminiResponseSchema(schema: unknown): Record<string, unknown>;
/**
* Map NeuroLink ChatMessage[] history into the @google/genai content format
* and push the entries onto a contents array.
*
* Used by the native Vertex Gemini and Google AI Studio paths to honor
* `options.conversationMessages` so multi-turn conversations (memory, loop
* REPL, agent flows) actually carry prior turns into the request.
*
* Behavior notes:
* - Only `user` and `assistant` roles are forwarded; system messages are
* expected to be wired via `systemInstruction`, and tool-call /
* tool-result roles only appear inside intra-call tool loops which build
* their own model/function entries.
* - String content is wrapped as a single `{ text }` part. Empty strings
* are skipped to avoid sending empty parts that some Gemini regions
* reject.
* - The current user input should be appended AFTER calling this helper
* so the prior turns appear first in chronological order.
*/
export declare function prependConversationMessages(contents: Array<{
role: string;
parts: unknown[];
}>, conversationMessages?: Array<ChatMessage | MinimalChatMessage>): void;
/**
* Convert a native Gemini `contents` array (the shape this module builds and
* `prependConversationMessages` emits) into the generic V3 prompt shape that
* caller model middleware operates on.
*
* Gemini has no wire-visible `system` role — `systemInstruction` rides
* separately on `config` — so this only ever emits `user` / `assistant` /
* `tool` messages. A `functionResponse`-only `user` entry becomes a V3
* `tool` message rather than `user`: `UserModelMessage.content` cannot carry
* a `ToolResultPart`, only `ToolModelMessage.content` can, and that grouping
* (a `model` turn of `functionCall` parts followed by a `user` turn of
* matching `functionResponse` parts) is exactly what
* `prependConversationMessages` above produces.
*/
export declare function geminiContentsToV3Prompt(contents: Array<{
role: string;
parts: unknown[];
}>): ModelMessage[];
/**
* Reverse of `geminiContentsToV3Prompt`: rebuild a native Gemini `contents`
* array from a (possibly middleware-rewritten) V3 prompt. A `system`
* message has no `contents` entry of its own — Gemini carries it on
* `config.systemInstruction` — so its text is returned separately
* (`systemText`) and the caller folds it into the request config.
*
* The synthesized `toolCallId` from the forward direction is intentionally
* discarded: Gemini pairs a call to its result by position, not by id, so
* reconstruction only needs `{name, args}` / `{name, response}`.
*/
export declare function v3PromptToGeminiContents(prompt: ModelMessage[]): {
contents: Array<{
role: string;
parts: unknown[];
}>;
systemText?: string;
};
/**
* Build the `parts` array for the current user turn of a Gemini native
* `generateContent` request, including inline image + PDF blobs.
*
* Both providers that hit the native `@google/genai` SDK — `GoogleVertex`
* and `GoogleAIStudio` — need this. The previous AI Studio code only
* pushed a single `{ text }` part, which silently dropped `input.images`
* and `input.pdfFiles` on the floor: the model received text only and
* legitimately reported "no image attached". Extracting this from the
* Vertex copy keeps both providers on one definition.
*
* Accepted shapes per element (mirroring the runtime behaviour the Vertex
* code already supported):
* - `Buffer` → used as-is
* - local file path → read via `readFileSync`, MIME guessed from extension
* - `data:<mime>;base64,...` URL → mime parsed, data base64-decoded
* - `http(s)://...` URL → fetched, mime from `content-type`
* - any other string → assumed to be a base64-encoded payload
*
* Image MIME guessing is conservative — only known extensions override the
* default `image/jpeg`. Fetch failures are logged and the offending entry
* is skipped rather than aborting the entire request, matching prior
* Vertex behaviour.
*/
/**
* Append audio to a Gemini request as `inlineData` parts.
*
* Shared by both Gemini front ends. Vertex assembles its request here and AI
* Studio assembles it in `buildUserPartsWithMultimodal`; when this lived only in
* the Vertex client, AI Studio advertised audio support through
* `NATIVE_AUDIO_PROVIDERS` and then silently dropped the bytes.
*
* Gemini's native request shape is assembled directly rather than taken from the
* AI SDK's `file` parts, so audio has to be added explicitly the same way PDFs
* and images are — a `{ type: "file" }` part built upstream simply never
* reaches this request body. That asymmetry is why attaching a recording
* produced only the metadata summary even after the message builder learned to
* carry the bytes.
*
* A container Gemini does not accept is converted first; one that cannot be
* converted is skipped rather than sent, because an unsupported inlineData
* mimeType fails the whole request, and the caller still has the metadata
* summary in the text part.
*/
export declare function appendNativeAudioParts(userParts: VertexNativePart[], audioFiles: MultimodalAudioEntry[] | undefined, logPrefix?: string): Promise<void>;
/**
* Append video to a Gemini request as `inlineData` parts.
*
* The native request body is assembled by hand rather than taken from the AI
* SDK's `file` parts, so — exactly as with audio — a `{ type: "file" }` part
* built upstream never arrives here. Video has to be added explicitly or it
* is silently dropped on both Gemini front ends while the capability table
* says it is supported.
*
* A clip the provider will not take inline is skipped rather than sent: an
* oversized or unsupported `inlineData` part fails the whole request, whereas
* skipping leaves the caller with the metadata summary and the keyframes,
* which is what it had before native delivery existed.
*
* `model` is forwarded to `canDeliverVideoNatively` so a Claude-on-Vertex
* request — same `providerName` ("vertex") as a Gemini-on-Vertex one, routed
* to the native Anthropic SDK instead by `GoogleVertexProvider.isAnthropicModel`
* — is answered as not natively video-capable rather than inheriting Vertex's
* Gemini row. Optional because AI Studio's caller never has a Claude model to
* pass.
*/
export declare function appendNativeVideoParts(userParts: VertexNativePart[], videoFiles: MultimodalVideoEntry[] | undefined, providerName: string, model?: string, logPrefix?: string): Promise<void>;
export declare function buildUserPartsWithMultimodal(input: GeminiMultimodalInput | undefined, textOverride?: string, logPrefix?: string, model?: string): Promise<VertexNativePart[]>;