@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
250 lines (249 loc) • 11.8 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 Tool } from "ai";
import type { ThinkingConfig, CollectedChunkResult, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../types/index.js";
export declare function sanitizeForGoogleFunctionName(name: string): string;
/**
* Resolve a sanitized Gemini tool name to one that is both unique within
* the current request and at most 128 characters. When the candidate
* collides with an already-used name we append `_2`, `_3`, … — but
* reserve room for the suffix by truncating the base first so the
* resolved name never exceeds Google's `function_declarations[].name`
* limit.
*
* @param base The already-sanitized candidate name.
* @param isTaken Predicate that returns true if `name` is already used.
*/
export declare function resolveUniqueGoogleFunctionName(base: string, isTaken: (name: string) => boolean): string;
/**
* Sanitize a JSON Schema for Gemini's proto-based API.
*
* Gemini cannot handle `anyOf`/`oneOf` union types in function declarations
* because its proto format expects a single `type` field, not a list of types.
* This function recursively converts unions to `string` type (the most
* permissive primitive that can represent any value as text).
*
* Also removes `$schema`, `additionalProperties`, and `default` keys that
* Gemini's proto format doesn't support.
*/
export declare function sanitizeSchemaForGemini(schema: Record<string, unknown>): Record<string, unknown>;
/**
* Sanitize Vercel AI SDK tools for Gemini compatibility.
*
* For the Vercel AI SDK path (non-native), tool parameters are Zod schemas that
* get converted to JSON Schema internally by @ai-sdk/google. This conversion
* doesn't sanitize union types (anyOf/oneOf), causing Gemini proto errors.
*
* This function pre-converts each tool's Zod parameters to sanitized JSON Schema
* and re-wraps with the Vercel AI SDK's jsonSchema() helper.
*/
export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): {
tools: Record<string, Tool>;
dropped: string[];
/**
* Reverse map: Google-safe sanitized name → original consumer-supplied
* name. Lets the calling layer translate tool-call results back so the
* sanitization stays transport-only (see CodeRabbit thread, PR #1006).
*/
originalNameMap: Map<string, string>;
};
export declare function normalizeToolsForJsonSchemaProvider(tools: Record<string, Tool>): {
tools: Record<string, Tool>;
normalized: string[];
};
/**
* 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>): NativeToolDeclarationsResult;
/**
* 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;
maxTokens?: number;
systemPrompt?: 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;
/**
* 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>;
/**
* Create a push-based text channel that bridges a background producer
* (the agentic tool-calling loop) with an async-iterable consumer.
*
* This enables truly incremental streaming: text parts are yielded to the
* caller as they arrive from the network, rather than being buffered until
* the model finishes generating.
*/
export declare function createTextChannel(): TextChannel;
/**
* 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: TextChannel): 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>;
}): Promise<NativeFunctionResponse[]>;
/**
* Handle maxSteps termination by producing a final text when the model
* was still calling tools when the step limit was reached.
*
* @param logLabel - Label for log messages (e.g. "[GoogleAIStudio]" or "[GoogleVertex]")
*/
export declare function handleMaxStepsTermination(logLabel: string, step: number, maxSteps: number, finalText: string, lastStepText: string): string;
/**
* 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<{
role: string;
content: string;
}>): void;
/**
* 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.
*/
export declare function buildUserPartsWithMultimodal(input: GeminiMultimodalInput | undefined, textOverride?: string, logPrefix?: string): Promise<VertexNativePart[]>;