@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
1,036 lines • 235 kB
JavaScript
/* eslint-disable max-lines-per-function */
// Native SDK imports - no more @ai-sdk/google-vertex dependency
import fs from "fs";
import path from "path";
import os from "os";
import {} from "ai";
import { AIProviderName, ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../core/constants.js";
import { ModelConfigurationManager } from "../core/modelConfiguration.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
import { FileDetector } from "../utils/fileDetector.js";
import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
import { logger } from "../utils/logger.js";
import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, } from "../utils/modelDetection.js";
import { validateApiKey, createVertexProjectConfig, createGoogleAuthConfig, } from "../utils/providerConfig.js";
import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } from "../utils/schemaConversion.js";
import { createNativeThinkingConfig } from "../utils/thinkingConfig.js";
import { TimeoutError, withTimeout } from "../utils/async/index.js";
import { parseTimeout } from "../utils/timeout.js";
import { createTextChannel, extractThoughtSignature, prependConversationMessages, } from "./googleNativeGemini3.js";
import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../telemetry/index.js";
import { calculateCost } from "../utils/pricing.js";
import { transformToolExecutions } from "../utils/transformationUtils.js";
// Import proper types for multimodal message handling
// Dynamic import helper for native Anthropic Vertex SDK
let anthropicVertexModule = null;
async function getAnthropicVertexModule() {
if (!anthropicVertexModule) {
anthropicVertexModule = await import("@anthropic-ai/vertex-sdk");
}
return anthropicVertexModule;
}
// Enhanced Anthropic support check - now uses native SDK
const hasAnthropicSupport = () => {
// Always return true as we have the native SDK available
// Actual availability is checked at runtime when creating the client
return true;
};
/**
* Recursively strip JSON-schema fields that Vertex Gemini's function-call
* validator rejects with 400 INVALID_ARGUMENT. Vertex implements OpenAPI 3.0
* Schema strictly and rejects extension fields that the broader JSON Schema
* spec allows. The fields stripped here have no semantic meaning for the
* model, so removing them is safe for every caller.
*
* Fields removed:
* - `additionalProperties` — extension; Vertex rejects on any nested object.
* - `default` — Vertex rejects defaults on object/array-typed properties and
* on properties that are also marked `required`. Safest to strip globally
* because the model never inspects them.
* - `$schema`, `$id`, `$ref`, `definitions`, `$defs` — JSON-Schema-meta
* fields that Vertex doesn't recognise.
* - `examples` — accepted by some Gemini variants but not 2.5-flash; strip
* to avoid the model rejecting tool schemas under that path.
*/
function stripAdditionalPropertiesDeep(schema) {
if (!schema || typeof schema !== "object") {
return;
}
const FIELDS_TO_STRIP = [
"additionalProperties",
"default",
"$schema",
"$id",
"$ref",
"definitions",
"$defs",
"examples",
];
for (const field of FIELDS_TO_STRIP) {
if (field in schema) {
delete schema[field];
}
}
// JSON Schema Draft-4 `exclusiveMinimum: true` / `exclusiveMaximum: true`
// (boolean form) is rejected by Vertex's OpenAPI 3.0 validator, which
// expects a numeric bound. zod-to-json-schema's openApi3 target still
// emits the Draft-4 form for `z.number().positive()` etc. Translate the
// boolean form into the numeric form when paired with `minimum` /
// `maximum`; otherwise drop it (the model doesn't validate, so the
// constraint is informational only).
if (typeof schema.exclusiveMinimum === "boolean") {
if (schema.exclusiveMinimum === true &&
typeof schema.minimum === "number") {
schema.exclusiveMinimum = schema.minimum;
delete schema.minimum;
}
else {
delete schema.exclusiveMinimum;
}
}
if (typeof schema.exclusiveMaximum === "boolean") {
if (schema.exclusiveMaximum === true &&
typeof schema.maximum === "number") {
schema.exclusiveMaximum = schema.maximum;
delete schema.maximum;
}
else {
delete schema.exclusiveMaximum;
}
}
// Strip `maximum` values that exceed int32 range — Vertex's protobuf
// serializer treats `type: "integer"` as int32 and rejects bounds beyond
// 2^31. zod's `.positive().int()` emits Number.MAX_SAFE_INTEGER as the
// upper bound (8.9e15), which trips this. The constraint is informational
// for the model anyway, so dropping it is safe.
const INT32_MAX = 2147483647;
if (typeof schema.maximum === "number" && schema.maximum > INT32_MAX) {
delete schema.maximum;
}
if (typeof schema.minimum === "number" && schema.minimum < -INT32_MAX) {
delete schema.minimum;
}
if (schema.properties && typeof schema.properties === "object") {
for (const child of Object.values(schema.properties)) {
if (child && typeof child === "object") {
stripAdditionalPropertiesDeep(child);
}
}
}
if (schema.items && typeof schema.items === "object") {
if (Array.isArray(schema.items)) {
for (const item of schema.items) {
if (item && typeof item === "object") {
stripAdditionalPropertiesDeep(item);
}
}
}
else {
stripAdditionalPropertiesDeep(schema.items);
}
}
for (const key of ["allOf", "anyOf", "oneOf"]) {
if (Array.isArray(schema[key])) {
for (const branch of schema[key]) {
if (branch && typeof branch === "object") {
stripAdditionalPropertiesDeep(branch);
}
}
}
}
}
// Configuration helpers - now using consolidated utility
const getVertexProjectId = () => {
return validateApiKey(createVertexProjectConfig());
};
const getVertexLocation = () => {
return (process.env.GOOGLE_CLOUD_LOCATION ||
process.env.VERTEX_LOCATION ||
process.env.GOOGLE_VERTEX_LOCATION ||
"us-central1");
};
/**
* Resolve the effective Vertex region for a given model.
*
* Policy (matches the bugfixes-suite contract):
* - Every Gemini model (`gemini-*`) is force-routed to the `global` endpoint
* regardless of any caller-supplied region. Regional endpoints 404 for
* Gemini 3.x previews and the regional/global behaviour for 2.x is
* consistent enough that pinning all Gemini traffic to global is the
* right safe default. The legacy `GLOBAL_LOCATION_MODELS` allowlist is
* kept as a defence-in-depth fallback so any non-`gemini-` identifiers
* that still need global (e.g. image-gen aliases) keep working.
* - Non-Gemini models (Claude on Vertex, embeddings, custom models) keep
* the caller-supplied region or fall back to env-derived defaults.
*
* @param modelName - The target model identifier.
* @param configuredLocation - Caller-provided region (e.g. options.region).
* Used as the fallback for non-Gemini models; ignored for Gemini.
* @returns The region string to pass to the @google/genai client.
*/
export const resolveVertexLocation = (modelName, configuredLocation) => {
const fallback = configuredLocation || getVertexLocation();
if (!modelName) {
return fallback;
}
const lower = modelName.toLowerCase();
const isGemini = lower.startsWith("gemini-") ||
lower.includes("/gemini-") ||
GLOBAL_LOCATION_MODELS.some((m) => lower === m.toLowerCase() ||
lower.includes(m.toLowerCase()) ||
m.toLowerCase().includes(lower));
if (isGemini) {
return process.env.GOOGLE_VERTEX_GLOBAL_LOCATION || "global";
}
return fallback;
};
/**
* Backwards-compatible internal alias kept so existing call sites compile
* unchanged. New code should call `resolveVertexLocation` directly.
*/
const resolveVertexRegionForModel = resolveVertexLocation;
const getDefaultVertexModel = () => {
// Use gemini-2.5-flash as default - latest and best price-performance model
// Override with VERTEX_MODEL environment variable if needed
return process.env.VERTEX_MODEL || "gemini-2.5-flash";
};
const hasGoogleCredentials = () => {
return !!(process.env.GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK ||
process.env.GOOGLE_APPLICATION_CREDENTIALS ||
process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
(process.env.GOOGLE_AUTH_CLIENT_EMAIL &&
process.env.GOOGLE_AUTH_PRIVATE_KEY));
};
// Cache the runtime-created credentials file path so we don't write a new file
// on every settings creation (which would leak files in /tmp). The file is also
// cleaned up on process exit.
let cachedRuntimeCredentialsFile = null;
let credentialsCleanupRegistered = false;
const registerCredentialsCleanup = (filePath) => {
if (credentialsCleanupRegistered) {
return;
}
credentialsCleanupRegistered = true;
const cleanup = () => {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
catch {
// Ignore cleanup errors — best-effort
}
};
process.once("exit", cleanup);
process.once("SIGINT", () => {
cleanup();
process.exit(130);
});
process.once("SIGTERM", () => {
cleanup();
process.exit(143);
});
};
// Enhanced Vertex settings creation with authentication fallback and proxy support
const createVertexSettings = async (region) => {
const location = region || getVertexLocation();
const project = getVertexProjectId();
const baseSettings = {
project,
location,
fetch: createProxyFetch(),
};
// Note: Global endpoint handling is managed by the @google/genai SDK based on location parameter.
// Authentication is handled via GOOGLE_APPLICATION_CREDENTIALS environment variable
// or the temporary credentials file approach below.
// 🎯 OPTION 2: Create credentials file from environment variables at runtime
// This solves the problem where GOOGLE_APPLICATION_CREDENTIALS exists in ZSHRC locally
// but the file doesn't exist on production servers
// First, try to create credentials file from individual environment variables
const requiredEnvVarsForFile = {
type: process.env.GOOGLE_AUTH_TYPE,
project_id: process.env.GOOGLE_AUTH_BREEZE_PROJECT_ID,
private_key: process.env.GOOGLE_AUTH_PRIVATE_KEY,
client_email: process.env.GOOGLE_AUTH_CLIENT_EMAIL,
client_id: process.env.GOOGLE_AUTH_CLIENT_ID,
auth_uri: process.env.GOOGLE_AUTH_AUTH_URI,
token_uri: process.env.GOOGLE_AUTH_TOKEN_URI,
auth_provider_x509_cert_url: process.env.GOOGLE_AUTH_AUTH_PROVIDER_CERT_URL,
client_x509_cert_url: process.env.GOOGLE_AUTH_CLIENT_CERT_URL,
universe_domain: process.env.GOOGLE_AUTH_UNIVERSE_DOMAIN,
};
// If we have the essential fields, create a runtime credentials file
// (or reuse the one we already wrote earlier in this process)
if (requiredEnvVarsForFile.client_email &&
requiredEnvVarsForFile.private_key) {
try {
// Reuse cached file if it still exists on disk
if (cachedRuntimeCredentialsFile &&
fs.existsSync(cachedRuntimeCredentialsFile)) {
process.env.GOOGLE_APPLICATION_CREDENTIALS =
cachedRuntimeCredentialsFile;
return baseSettings;
}
// Build complete service account credentials object
const serviceAccountCredentials = {
type: requiredEnvVarsForFile.type || "service_account",
project_id: requiredEnvVarsForFile.project_id || getVertexProjectId(),
private_key: requiredEnvVarsForFile.private_key.replace(/\\n/g, "\n"),
client_email: requiredEnvVarsForFile.client_email,
client_id: requiredEnvVarsForFile.client_id || "",
auth_uri: requiredEnvVarsForFile.auth_uri ||
"https://accounts.google.com/o/oauth2/auth",
token_uri: requiredEnvVarsForFile.token_uri ||
"https://oauth2.googleapis.com/token",
auth_provider_x509_cert_url: requiredEnvVarsForFile.auth_provider_x509_cert_url ||
"https://www.googleapis.com/oauth2/v1/certs",
client_x509_cert_url: requiredEnvVarsForFile.client_x509_cert_url || "",
universe_domain: requiredEnvVarsForFile.universe_domain || "googleapis.com",
};
// Create temporary credentials file (once per process)
const tmpDir = os.tmpdir();
const credentialsFileName = `google-credentials-${Date.now()}-${Math.random().toString(36).substring(2, 11)}.json`;
const credentialsFilePath = path.join(tmpDir, credentialsFileName);
fs.writeFileSync(credentialsFilePath, JSON.stringify(serviceAccountCredentials, null, 2),
// Owner read/write only — credentials should not be world-readable
{ mode: 0o600 });
// Set the environment variable to point to our runtime-created file
process.env.GOOGLE_APPLICATION_CREDENTIALS = credentialsFilePath;
cachedRuntimeCredentialsFile = credentialsFilePath;
registerCredentialsCleanup(credentialsFilePath);
// Now continue with the normal flow - check if the file exists
const fileExists = fs.existsSync(credentialsFilePath);
if (fileExists) {
return baseSettings;
}
}
catch {
// Silent error handling for runtime credentials file creation
}
}
// 🎯 OPTION 1: Check for principal account authentication (Accept any valid GOOGLE_APPLICATION_CREDENTIALS file (service account OR ADC))
if (process.env.GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK) {
const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK;
// Check if the credentials file exists
let fileExists = false;
try {
fileExists = fs.existsSync(credentialsPath);
}
catch {
// fileExists remains false
}
if (fileExists) {
return baseSettings;
}
}
else {
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
// Check if the credentials file exists
let fileExists = false;
try {
fileExists = fs.existsSync(credentialsPath);
}
catch {
// fileExists remains false
}
if (fileExists) {
return baseSettings;
}
}
}
// Log warning if no valid authentication is available
// Note: Authentication is handled via GOOGLE_APPLICATION_CREDENTIALS environment variable
// or the temporary credentials file approach (OPTION 2 above).
logger.warn("No valid authentication found for Google Vertex AI", {
authMethod: "none",
authenticationAttempts: {
principalAccountFile: {
envVarSet: !!process.env.GOOGLE_APPLICATION_CREDENTIALS,
filePath: process.env.GOOGLE_APPLICATION_CREDENTIALS || "NOT_SET",
fileExists: false, // We already checked above
},
explicitCredentials: {
hasClientEmail: !!process.env.GOOGLE_AUTH_CLIENT_EMAIL,
hasPrivateKey: !!process.env.GOOGLE_AUTH_PRIVATE_KEY,
},
},
troubleshooting: [
"1. Ensure GOOGLE_APPLICATION_CREDENTIALS points to an existing file, OR",
"2. Set individual environment variables: GOOGLE_AUTH_CLIENT_EMAIL and GOOGLE_AUTH_PRIVATE_KEY",
],
});
return baseSettings;
};
// Create Anthropic-specific Vertex settings for native @anthropic-ai/vertex-sdk
const createVertexAnthropicSettings = async (region) => {
const location = region || getVertexLocation();
const project = getVertexProjectId();
return {
projectId: project,
region: location,
};
};
// Helper function to determine if a model is an Anthropic model
const isAnthropicModel = (modelName) => {
return modelName.toLowerCase().includes("claude");
};
/**
* Google Vertex AI Provider v2 - BaseProvider Implementation
*
* Features:
* - Extends BaseProvider for shared functionality
* - Preserves existing Google Cloud authentication
* - Maintains Anthropic model support via dynamic imports
* - Fresh model creation for each request
* - Enhanced error handling with setup guidance
* - Tool registration and context management
*
* @important Tools + Schema Support (Fixed)
* Gemini models on Vertex AI now support combining function calling (tools) with
* structured output (JSON schema) simultaneously. The fix works by NOT setting
* `responseMimeType: "application/json"` when tools are present, which was
* causing the Google API error.
*
* The `responseSchema` is still set to guide the output structure, allowing
* tools to execute AND the final output to follow the schema format.
*
* @example Gemini models with tools + schemas
* ```typescript
* const provider = new GoogleVertexProvider("gemini-2.5-flash");
* const result = await provider.generate({
* input: { text: "Analyze data using tools" },
* schema: MySchema,
* output: { format: "json" },
* // No need for disableTools: true anymore!
* });
* ```
*
* @example Claude models (always supported both)
* ```typescript
* const provider = new GoogleVertexProvider("claude-3-5-sonnet-20241022");
* const result = await provider.generate({
* input: { text: "Analyze data" },
* schema: MySchema,
* output: { format: "json" }
* });
* ```
*
* @note "Too many states for serving" errors can still occur with very complex schemas + tools.
* Solution: Simplify schema or reduce number of tools if this occurs.
* @see https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models
*/
export class GoogleVertexProvider extends BaseProvider {
projectId;
location;
registeredTools = new Map();
toolContext = {};
// Memory-managed cache for model configuration lookups to avoid repeated calls
// Uses WeakMap for automatic cleanup and bounded LRU for recently used models
static modelConfigCache = new Map();
static modelConfigCacheTime = 0;
static CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
static MAX_CACHE_SIZE = 50; // Prevent memory leaks by limiting cache size
// Memory-managed cache for maxTokens handling decisions to optimize streaming performance
static maxTokensCache = new Map();
static maxTokensCacheTime = 0;
constructor(modelName, _providerName, sdk, region, credentials) {
super(modelName, "vertex", sdk);
// Apply per-request credentials if provided
if (credentials) {
if (credentials.projectId) {
process.env.GOOGLE_CLOUD_PROJECT = String(credentials.projectId);
}
if (credentials.location) {
process.env.GOOGLE_CLOUD_LOCATION = String(credentials.location);
}
if (credentials.apiKey) {
process.env.GOOGLE_API_KEY = String(credentials.apiKey);
}
}
// Validate Google Cloud credentials - now using consolidated utility
if (!hasGoogleCredentials()) {
validateApiKey(createGoogleAuthConfig());
}
// Initialize Google Cloud configuration
this.projectId = credentials?.projectId || getVertexProjectId();
this.location =
region || credentials?.location || getVertexLocation();
logger.debug("[GoogleVertexProvider] Constructor initialized", {
regionParam: region,
resolvedLocation: this.location,
projectId: this.projectId,
});
logger.debug("Google Vertex AI BaseProvider v2 initialized", {
modelName: this.modelName,
projectId: this.projectId,
location: this.location,
provider: this.providerName,
});
}
getProviderName() {
return "vertex";
}
getDefaultModel() {
return getDefaultVertexModel();
}
/**
* Returns the Vercel AI SDK model instance for Google Vertex
* Creates fresh model instances for each request
*/
async getAISDKModel() {
// This method is no longer used - we route ALL models directly to native SDKs
// in executeStream and generate methods. Throwing an error to catch any
// unexpected code paths that might try to use the old Vercel AI SDK approach.
throw new NeuroLinkError({
code: ERROR_CODES.INVALID_CONFIGURATION,
message: "GoogleVertexProvider no longer uses @ai-sdk/google-vertex. All models use native SDKs: @google/genai for Gemini, @anthropic-ai/vertex-sdk for Claude.",
category: ErrorCategory.CONFIGURATION,
severity: ErrorSeverity.CRITICAL,
retriable: false,
context: { provider: this.providerName, model: this.modelName },
});
}
/**
* Initialize model creation tracking
*/
initializeModelCreationLogging() {
const modelCreationId = `vertex-model-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
const modelCreationStartTime = Date.now();
const modelCreationHrTimeStart = process.hrtime.bigint();
const modelName = this.modelName || getDefaultVertexModel();
return {
modelCreationId,
modelCreationStartTime,
modelCreationHrTimeStart,
modelName,
};
}
/**
* Check if model is Anthropic-based and attempt creation
*/
async attemptAnthropicModelCreation(modelName, modelCreationId, modelCreationStartTime, modelCreationHrTimeStart) {
const isAnthropic = isAnthropicModel(modelName);
if (!isAnthropic) {
return null;
}
logger.debug("Creating Anthropic model using vertexAnthropic provider", {
modelName,
});
if (!hasAnthropicSupport()) {
logger.warn(`[GoogleVertexProvider] Anthropic support not available, falling back to Google model`);
return null;
}
try {
const anthropicModel = await this.createAnthropicModel(modelName);
if (anthropicModel) {
return anthropicModel;
}
// Anthropic model creation returned null, falling back to Google model
}
catch (error) {
logger.error(`[GoogleVertexProvider] ❌ LOG_POINT_V006_ANTHROPIC_MODEL_ERROR`, {
logPoint: "V006_ANTHROPIC_MODEL_ERROR",
modelCreationId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - modelCreationStartTime,
elapsedNs: (process.hrtime.bigint() - modelCreationHrTimeStart).toString(),
modelName,
error: error instanceof Error ? error.message : String(error),
errorName: error instanceof Error ? error.name : "UnknownError",
errorStack: error instanceof Error ? error.stack : undefined,
fallbackToGoogle: true,
message: "Anthropic model creation failed - falling back to Google model",
});
}
// Fall back to regular model if Anthropic not available
logger.warn(`Anthropic model ${modelName} requested but not available, falling back to Google model`);
return null;
}
/**
* Create Google Vertex model with comprehensive logging and error handling
*/
async createGoogleVertexModel(modelName, modelCreationId, modelCreationStartTime, modelCreationHrTimeStart) {
logger.debug("Creating Google Vertex model", {
modelName,
project: this.projectId,
location: this.location,
});
const vertexSettingsStartTime = process.hrtime.bigint();
logger.debug(`[GoogleVertexProvider] ⚙️ LOG_POINT_V008_VERTEX_SETTINGS_START`, {
logPoint: "V008_VERTEX_SETTINGS_START",
modelCreationId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - modelCreationStartTime,
elapsedNs: (process.hrtime.bigint() - modelCreationHrTimeStart).toString(),
vertexSettingsStartTimeNs: vertexSettingsStartTime.toString(),
// Network configuration analysis
networkConfig: {
projectId: this.projectId,
location: this.location,
expectedEndpoint: `https://${this.location}-aiplatform.googleapis.com`,
httpProxy: process.env.HTTP_PROXY || process.env.http_proxy,
httpsProxy: process.env.HTTPS_PROXY || process.env.https_proxy,
noProxy: process.env.NO_PROXY || process.env.no_proxy,
proxyConfigured: !!(process.env.HTTP_PROXY ||
process.env.HTTPS_PROXY ||
process.env.http_proxy ||
process.env.https_proxy),
},
message: "Starting Vertex settings creation with network configuration analysis",
});
try {
const vertexSettings = await createVertexSettings(this.location);
const vertexSettingsEndTime = process.hrtime.bigint();
const vertexSettingsDurationNs = vertexSettingsEndTime - vertexSettingsStartTime;
logger.debug(`[GoogleVertexProvider] ✅ LOG_POINT_V009_VERTEX_SETTINGS_SUCCESS`, {
logPoint: "V009_VERTEX_SETTINGS_SUCCESS",
modelCreationId,
timestamp: new Date().toISOString(),
elapsedMs: Date.now() - modelCreationStartTime,
elapsedNs: (process.hrtime.bigint() - modelCreationHrTimeStart).toString(),
vertexSettingsDurationNs: vertexSettingsDurationNs.toString(),
vertexSettingsDurationMs: Number(vertexSettingsDurationNs) / 1000000,
// Settings analysis
vertexSettingsAnalysis: {
hasSettings: !!vertexSettings,
settingsType: typeof vertexSettings,
settingsKeys: vertexSettings ? Object.keys(vertexSettings) : [],
projectId: vertexSettings?.project,
location: vertexSettings?.location,
hasFetch: !!vertexSettings?.fetch,
settingsSize: vertexSettings
? JSON.stringify(vertexSettings).length
: 0,
},
message: "Vertex settings created successfully",
});
return await this.createVertexInstance(vertexSettings, modelName, modelCreationId, modelCreationStartTime, modelCreationHrTimeStart);
}
catch (error) {
const vertexSettingsErrorTime = process.hrtime.bigint();
const vertexSettingsDurationNs = vertexSettingsErrorTime - vertexSettingsStartTime;
const totalErrorDurationNs = vertexSettingsErrorTime - modelCreationHrTimeStart;
logger.error(`[GoogleVertexProvider] ❌ LOG_POINT_V014_VERTEX_SETTINGS_ERROR`, {
logPoint: "V014_VERTEX_SETTINGS_ERROR",
modelCreationId,
timestamp: new Date().toISOString(),
totalElapsedMs: Date.now() - modelCreationStartTime,
totalElapsedNs: totalErrorDurationNs.toString(),
totalErrorDurationMs: Number(totalErrorDurationNs) / 1000000,
vertexSettingsDurationNs: vertexSettingsDurationNs.toString(),
vertexSettingsDurationMs: Number(vertexSettingsDurationNs) / 1000000,
// Comprehensive error analysis
error: error instanceof Error ? error.message : String(error),
errorName: error instanceof Error ? error.name : "UnknownError",
errorStack: error instanceof Error ? error.stack : undefined,
// Network diagnostic information
networkDiagnostics: {
errorCode: error?.code || "UNKNOWN",
errorErrno: error?.errno || "UNKNOWN",
errorAddress: error?.address || "UNKNOWN",
errorPort: error?.port || "UNKNOWN",
errorSyscall: error?.syscall || "UNKNOWN",
errorHostname: error?.hostname || "UNKNOWN",
isTimeoutError: error instanceof Error &&
(error.message.includes("timeout") ||
error.message.includes("ETIMEDOUT")),
isNetworkError: error instanceof Error &&
(error.message.includes("ENOTFOUND") ||
error.message.includes("ECONNREFUSED") ||
error.message.includes("ETIMEDOUT")),
isAuthError: error instanceof Error &&
(error.message.includes("PERMISSION_DENIED") ||
error.message.includes("401") ||
error.message.includes("403")),
infrastructureIssue: error instanceof Error &&
error.message.includes("ETIMEDOUT") &&
error.message.includes("aiplatform.googleapis.com"),
},
// Environment at error time
errorEnvironment: {
httpProxy: process.env.HTTP_PROXY || process.env.http_proxy,
httpsProxy: process.env.HTTPS_PROXY || process.env.https_proxy,
googleAppCreds: process.env.GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK ||
process.env.GOOGLE_APPLICATION_CREDENTIALS ||
"NOT_SET",
hasGoogleServiceKey: !!process.env.GOOGLE_SERVICE_ACCOUNT_KEY,
nodeVersion: process.version,
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
},
message: "Vertex settings creation failed - critical network/authentication error",
});
throw error;
}
}
/**
* @deprecated This method is no longer used. All models now use native SDKs.
*/
async createVertexInstance(_vertexSettings, _modelName, _modelCreationId, _modelCreationStartTime, _modelCreationHrTimeStart) {
// This method is dead code - all models now route to native SDK methods.
throw new NeuroLinkError({
code: ERROR_CODES.INVALID_CONFIGURATION,
message: "createVertexInstance is deprecated. Use executeNativeGemini3Stream/Generate or executeNativeAnthropicStream/Generate instead.",
category: ErrorCategory.CONFIGURATION,
severity: ErrorSeverity.CRITICAL,
retriable: false,
context: { provider: this.providerName },
});
}
/**
* Gets the appropriate model instance (Google or Anthropic)
* Uses dual provider architecture for proper model routing
* Creates fresh instances for each request to ensure proper authentication
*/
async getModel() {
// Initialize logging and setup
const { modelCreationId, modelCreationStartTime, modelCreationHrTimeStart, modelName, } = this.initializeModelCreationLogging();
// Check if this is an Anthropic model and attempt creation
const anthropicModel = await this.attemptAnthropicModelCreation(modelName, modelCreationId, modelCreationStartTime, modelCreationHrTimeStart);
if (anthropicModel) {
return anthropicModel;
}
// Fall back to Google Vertex model creation
return await this.createGoogleVertexModel(modelName, modelCreationId, modelCreationStartTime, modelCreationHrTimeStart);
}
// executeGenerate removed - BaseProvider handles all generation with tools
/**
* Validate stream options
*/
validateStreamOptionsOnly(options) {
this.validateStreamOptions(options);
}
async executeStream(options, _analysisSchema) {
// ALL models now use native SDKs - no more @ai-sdk/google-vertex dependency
const modelName = options.model || this.modelName || getDefaultVertexModel();
// Wrap the native stream path in a `neurolink.provider.stream` span so
// the test:tracing observability harness sees the same span hierarchy
// it sees for AI Studio. BaseProvider.stream does NOT emit this span
// for any provider — each native provider has to add it itself.
return withClientStreamSpan({
name: "neurolink.provider.stream",
tracer: tracers.provider,
attributes: {
[ATTR.GEN_AI_SYSTEM]: this.providerName,
[ATTR.GEN_AI_MODEL]: modelName,
[ATTR.GEN_AI_OPERATION]: "stream",
[ATTR.NL_PROVIDER]: this.providerName,
},
}, async (streamSpan) => {
const streamStartTime = Date.now();
// Tool filter (a0269210): trust options.tools — caller (BaseProvider.stream)
// already merged MCP/built-in tools and applied any enabledToolNames filter.
const optionTools = options.tools || {};
// Emit a `neurolink.message.build` span for the native stream path
// so observability tooling sees the same hierarchy it sees on
// Pipeline A. Without this, test:tracing's "Message Build Span"
// assertion has to skip on every native-Vertex stream.
const processedOptions = await withSpan({
name: "neurolink.message.build",
tracer: tracers.provider,
attributes: {
[ATTR.NL_PROVIDER]: this.providerName,
"message.count": 1,
"message.build.count": 1,
"message.build.path": "vertex.native.stream",
},
}, async () => this.processCSVFilesForNativeSDK(options));
// Pass through to native SDK path
const mergedOptions = {
...processedOptions,
tools: optionTools,
};
try {
// Route Claude models to native Anthropic SDK
let result;
if (isAnthropicModel(modelName)) {
logger.info("[GoogleVertex] Routing Claude model to native @anthropic-ai/vertex-sdk", {
model: modelName,
totalToolCount: Object.keys(optionTools).length,
});
result = await this.executeNativeAnthropicStream(mergedOptions);
}
else {
// ALL Gemini models use native @google/genai SDK
logger.info("[GoogleVertex] Routing Gemini model to native @google/genai", {
model: modelName,
totalToolCount: Object.keys(optionTools).length,
});
result = await this.executeNativeGemini3Stream(mergedOptions);
}
// Cost / token usage on the stream span. Native streams resolve
// usage synchronously (the stream loop has already drained), so
// `result.usage` is populated by the time we reach this point.
this.attachUsageAndCostAttributes(streamSpan, modelName, result?.usage);
// Wrap the result's async iterable to fire onChunk / onFinish
// lifecycle callbacks. Pipeline A gets these via the AI SDK
// wrapStream middleware; the native path has to fire them here.
const wrappedResult = this.wrapStreamResultWithLifecycle(options, result, streamStartTime);
this.emitStreamEnd(modelName, streamStartTime, true);
return wrappedResult;
}
catch (error) {
this.fireGenerateOnError(options, error, streamStartTime);
this.emitStreamEnd(modelName, streamStartTime, false, error);
throw error;
}
}, (r) => r.stream, (r, wrapped) => ({ ...r, stream: wrapped }));
}
/**
* Emit `stream:end` so the Pipeline B observability listener creates a
* `model.generation` span for native Vertex stream traffic. Mirrors
* `emitGenerationEnd` (used by `generate()`).
*/
emitStreamEnd(modelName, startTime, success, error) {
const emitter = this.neurolink?.getEventEmitter();
if (!emitter) {
return;
}
emitter.emit("stream:end", {
provider: this.providerName,
responseTime: Date.now() - startTime,
timestamp: Date.now(),
result: {
content: "",
usage: { input: 0, output: 0, total: 0 },
model: modelName,
provider: this.providerName,
finishReason: success ? "stop" : "error",
},
success,
...(error
? { error: error instanceof Error ? error.message : String(error) }
: {}),
});
}
/**
* Create @google/genai client configured for Vertex AI
*/
async createVertexGenAIClient(regionOverride) {
const project = getVertexProjectId();
const location = regionOverride || this.location || getVertexLocation();
const mod = await import("@google/genai");
const ctor = mod.GoogleGenAI;
if (!ctor) {
throw new NeuroLinkError({
code: ERROR_CODES.INVALID_CONFIGURATION,
message: "@google/genai does not export GoogleGenAI",
category: ErrorCategory.CONFIGURATION,
severity: ErrorSeverity.CRITICAL,
retriable: false,
context: { module: "@google/genai", expectedExport: "GoogleGenAI" },
});
}
const Ctor = ctor;
// Use vertexai mode with project and location
// Include httpOptions with proxy fetch for corporate network support
return new Ctor({
vertexai: true,
project,
location,
httpOptions: {
fetch: createProxyFetch(),
},
});
}
/**
* Execute stream using native @google/genai SDK for Gemini 3 models on Vertex AI
* This bypasses @ai-sdk/google-vertex to properly handle thought_signature
*/
async executeNativeGemini3Stream(options) {
const modelName = options.model || this.modelName || getDefaultVertexModel();
const effectiveLocation = resolveVertexRegionForModel(modelName, options.region);
const client = await this.createVertexGenAIClient(effectiveLocation);
logger.debug("[GoogleVertex] Using native @google/genai for Gemini 3", {
model: modelName,
hasTools: !!options.tools && Object.keys(options.tools).length > 0,
project: this.projectId,
location: effectiveLocation,
});
// Build contents from input with multimodal support
const contents = [];
// Build user message parts - start with text.
// `options.input.text` is `string | undefined` in strict mode; the
// VertexNativePart `text` field requires `string`, so coerce to "" if
// unset (the multimodal-only path still appends other parts below).
const userParts = [{ text: options.input.text ?? "" }];
// Add PDF files as inlineData parts if present
// Cast input to access multimodal properties that may exist at runtime
const multimodalInput = options.input;
if (multimodalInput?.pdfFiles && multimodalInput.pdfFiles.length > 0) {
logger.debug(`[GoogleVertex] Processing ${multimodalInput.pdfFiles.length} PDF file(s) for native stream`);
for (const pdfFile of multimodalInput.pdfFiles) {
let pdfBuffer;
if (typeof pdfFile === "string") {
// Check if it's a file path
if (fs.existsSync(pdfFile)) {
pdfBuffer = fs.readFileSync(pdfFile);
}
else {
// Assume it's already base64 encoded
pdfBuffer = Buffer.from(pdfFile, "base64");
}
}
else {
pdfBuffer = pdfFile;
}
// Convert to base64 for the native SDK
const base64Data = pdfBuffer.toString("base64");
userParts.push({
inlineData: {
mimeType: "application/pdf",
data: base64Data,
},
});
}
}
// Add images as inlineData parts if present
if (multimodalInput?.images && multimodalInput.images.length > 0) {
logger.debug(`[GoogleVertex] Processing ${multimodalInput.images.length} image(s) for native stream`);
for (const image of multimodalInput.images) {
let imageBuffer;
let mimeType = "image/jpeg"; // Default
if (typeof image === "string") {
if (fs.existsSync(image)) {
imageBuffer = fs.readFileSync(image);
// Detect mime type from extension
const ext = path.extname(image).toLowerCase();
if (ext === ".png") {
mimeType = "image/png";
}
else if (ext === ".gif") {
mimeType = "image/gif";
}
else if (ext === ".webp") {
mimeType = "image/webp";
}
}
else if (image.startsWith("data:")) {
// Handle data URL
const matches = image.match(/^data:([^;]+);base64,(.+)$/);
if (matches) {
mimeType = matches[1];
imageBuffer = Buffer.from(matches[2], "base64");
}
else {
continue; // Skip invalid data URL
}
}
else if (image.startsWith("http://") ||
image.startsWith("https://")) {
// Image URL — fetch and base64-encode. Without this, the URL
// string falls through to the "assume base64" branch below
// and Vertex returns "Provided image is not valid".
try {
const response = await fetch(image);
if (!response.ok) {
logger.warn(`[GoogleVertex] Image fetch failed: ${response.status} ${response.statusText}, skipping`, { url: image });
continue;
}
const arrayBuffer = await response.arrayBuffer();
imageBuffer = Buffer.from(arrayBuffer);
const headerMime = response.headers.get("content-type");
if (headerMime && headerMime.startsWith("image/")) {
mimeType = headerMime.split(";")[0];
}
}
catch (fetchError) {
logger.warn(`[GoogleVertex] Image URL fetch threw, skipping: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`, { url: image });
continue;
}
}
else {
// Assume base64 string
imageBuffer = Buffer.from(image, "base64");
}
}
else {
imageBuffer = image;
}
const base64Data = imageBuffer.toString("base64");
userParts.push({
inlineData: {
mimeType,
data: base64Data,
},
});
}
}
// Prepend prior conversation turns before the current user message so
// multi-turn callers (memory, loop REPL, agent flows) actually carry
// context. Without this, the native Vertex Gemini stream rebuilt the
// contents from only the current input on every call.
prependConversationMessages(contents, options.conversationMessages);
contents.push({
role: "user",
parts: userParts,
});
// Convert Vercel AI SDK tools to @google/genai FunctionDeclarations
let tools;
const executeMap = new Map();
if (options.tools &&
Object.keys(options.tools).length > 0 &&
!options.disableTools) {
const functionDeclarations = [];
for (const [name, tool] of Object.entries(options.tools)) {
const decl = {
name,
description: tool.description || `Tool: ${name}`,
};
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
const legacyTool = tool;
const toolParams = legacyTool.parameters || tool.inputSchema;
if (toolParams) {
// Convert and inline schema to resolve $ref/definitions
const rawSchema = convertZodToJsonSchema(toolParams, "openApi3");
const inlinedSchema = inlineJsonSchema(rawSchema);
// Remove $schema if present - @google/genai doesn't need it
if (inlinedSchema.$schema) {
delete inlinedSchema.$schema;
}
// CRITICAL: Google Vertex AI requires ALL nested schemas to have a type field
// ensureNestedSchemaTypes recursively adds missing type fields to tool schemas
// Note: convertZodToJsonSchema now uses openApi3 target which produces nullable: true
const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
// Strip `additionalProperties` recursively — Vertex Gemini's
// function-call validator rejects it on object schemas (returns
// 400 INVALID_ARGUMENT) even though it's valid OpenAPI 3. The
// field has no semantic meaning to the model, so dropping it
// before send is safe for every caller.
stripAdditionalPropertiesDeep(typedSchema);
decl.parametersJsonSchema = typedSchema;
}
functionDeclarations.push(decl);
if (tool.execute) {
executeMap.set(name, tool.execute);
}
}
tools = [{ functionDeclarations }];
logger.debug("[GoogleVertex] Converted tools for native SDK", {
toolCount: functionDeclarations.length,
toolNames: functionDeclarations.map((t) => t.name),
});
}
// Check if we need to use the final_result tool pattern for structured output with tools
// When both schema AND tools are present, we add final_result as a tool
const streamOptions = options;
let useFinalResultTool = false;
if (streamOptions.schema && tools) {
useFinalResultTool = true;
// Convert schema to JSON schema format
const schemaAsJson = convertZodToJsonSchema(streamOptions.schema, "openApi3");
const inlinedSchema = inlineJsonSchema(schemaAsJson);
if (inlinedSchema.$schema) {
delete inlinedSchema.$schema;
}
const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
// Add final_result tool to the existing function declarations
const existingDeclarations = tools[0]?.functionDeclarations || [];
existingDeclarations.push({
name: "final_result",
description: "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structu