UNPKG

@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
/* 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