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,037 lines (1,036 loc) 216 kB
// Native SDK imports - no more @ai-sdk/google-vertex dependency import fs from "fs"; import path from "path"; import os from "os"; import { 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, } 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 } from "../utils/async/index.js"; import { prependConversationMessages } from "./googleNativeGemini3.js"; import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../telemetry/index.js"; import { calculateCost } from "../utils/pricing.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 structured data matching the expected schema.", parametersJsonSchema: typedSchema, }); tools = [{ functionDeclarations: existingDeclarations }]; logger.debug("[GoogleVertex] Added final_result tool for structured output with tools (stream)", {