UNPKG

@guardaian/sdk

Version:

Zero-friction AI governance and monitoring SDK for Node.js applications

286 lines (247 loc) 9.61 kB
/** * Anthropic Claude API Interceptor - Bulletproof Production Version * * CRITICAL GUARANTEES: * 1. Customer's Claude calls ALWAYS work, even if GuardAIan is down * 2. Zero performance impact on customer's AI operations * 3. All tracking errors are isolated and non-blocking * 4. Transparent operation - customer never knows we're monitoring */ function patchAnthropic(Anthropic, guardaianClient) { // Early validation with error protection if (!Anthropic || typeof Anthropic !== 'function') { safeLog(guardaianClient, '⚠️ Invalid Anthropic module - skipping patch (non-blocking)'); return; } // Prevent double-patching if (Anthropic.__guardaianPatched) { safeLog(guardaianClient, '⚠️ Anthropic already patched - skipping'); return; } try { safeLog(guardaianClient, '🔧 Patching Anthropic SDK with bulletproof protection...'); // Patch messages create endpoint patchMessagesCreate(Anthropic, guardaianClient); // Mark as patched Anthropic.__guardaianPatched = true; safeLog(guardaianClient, '✅ Anthropic SDK patched successfully with failsafe protection'); } catch (patchError) { // Even patching errors should not break customer code safeLog(guardaianClient, `⚠️ Anthropic patching failed (non-blocking): ${patchError.message}`); } } /** * Patch Anthropic Messages Create endpoint with bulletproof error handling */ function patchMessagesCreate(Anthropic, guardaianClient) { try { const originalCreate = Anthropic.prototype.messages?.create; if (!originalCreate) { safeLog(guardaianClient, '⚠️ Anthropic messages.create not found - skipping patch'); return; } Anthropic.prototype.messages.create = async function(params, options) { const startTime = Date.now(); // CRITICAL: Always call original Anthropic API first // Customer's Claude call must NEVER depend on GuardAIan let response, originalError; try { // Execute customer's actual Anthropic API call response = await originalCreate.call(this, params, options); } catch (error) { // Store the original error to re-throw later originalError = error; } // CRITICAL: Track usage in completely isolated context // This happens asynchronously and never affects the customer's response setImmediate(() => { try { trackAnthropicUsage(params, response, originalError, startTime, guardaianClient); } catch (trackingError) { // Tracking errors are completely isolated from customer code safeLog(guardaianClient, `❌ Anthropic tracking error (isolated): ${trackingError.message}`); } }); // CRITICAL: Always return original result or throw original error // Customer gets exactly what they would get without GuardAIan if (originalError) { throw originalError; } return response; }; } catch (patchError) { safeLog(guardaianClient, `❌ Failed to patch Anthropic messages: ${patchError.message}`); } } /** * Track Anthropic usage with complete error isolation */ async function trackAnthropicUsage(params, response, error, startTime, guardaianClient) { try { const endTime = Date.now(); const duration = endTime - startTime; if (response) { // Successful request tracking const usage = response.usage || {}; const cost = calculateAnthropicCost(params.model, usage); // Fire-and-forget tracking call guardaianClient.track({ service: 'anthropic', model: params.model || 'claude-3-sonnet', operation: 'messages.create', inputTokens: usage.input_tokens || 0, outputTokens: usage.output_tokens || 0, totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0), cost: cost, duration: duration, requestData: sanitizeAnthropicRequest(params), responseData: sanitizeAnthropicResponse(response), metadata: { success: true, stopReason: response.stop_reason, hasUsage: !!response.usage } }).catch(trackingError => { // Even the track() call errors are isolated safeLog(guardaianClient, `❌ Anthropic track call failed (isolated): ${trackingError.message}`); }); safeLog(guardaianClient, `✅ Tracked Anthropic call: ${params.model} (${(usage.input_tokens || 0) + (usage.output_tokens || 0)} tokens, ${cost.toFixed(6)})`); } else if (error) { // Failed request tracking (for monitoring) guardaianClient.track({ service: 'anthropic', model: params.model || 'claude-3-sonnet', operation: 'messages.create', inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, duration: duration, requestData: sanitizeAnthropicRequest(params), responseData: { error: true, status: error.status || 'unknown', type: error.type || 'api_error', message: error.message?.substring(0, 200) || 'Unknown error' }, metadata: { success: false, errorType: error.constructor.name } }).catch(trackingError => { // Tracking errors for failed requests are also isolated safeLog(guardaianClient, `❌ Anthropic error tracking failed (isolated): ${trackingError.message}`); }); safeLog(guardaianClient, `📊 Tracked Anthropic error: ${params.model} - ${error.message}`); } } catch (trackingError) { // Ultimate safety net - even this function can fail safely safeLog(guardaianClient, `❌ Anthropic usage tracking completely failed (isolated): ${trackingError.message}`); } } /** * Calculate cost for Anthropic API calls with error protection */ function calculateAnthropicCost(model, usage) { try { // Latest Anthropic pricing (as of 2024) const pricing = { 'claude-3-5-sonnet-20241022': { input: 0.003, output: 0.015 }, 'claude-3-5-haiku-20241022': { input: 0.001, output: 0.005 }, 'claude-3-opus-20240229': { input: 0.015, output: 0.075 }, 'claude-3-sonnet-20240229': { input: 0.003, output: 0.015 }, 'claude-3-haiku-20240307': { input: 0.00025, output: 0.00125 }, // Legacy model names 'claude-3-5-sonnet': { input: 0.003, output: 0.015 }, 'claude-3-5-haiku': { input: 0.001, output: 0.005 }, 'claude-3-opus': { input: 0.015, output: 0.075 }, 'claude-3-sonnet': { input: 0.003, output: 0.015 }, 'claude-3-haiku': { input: 0.00025, output: 0.00125 } }; // Default to Claude 3 Sonnet pricing if model not found const modelPricing = pricing[model] || pricing['claude-3-sonnet-20240229']; const inputCost = (usage.input_tokens || 0) * modelPricing.input / 1000; const outputCost = (usage.output_tokens || 0) * modelPricing.output / 1000; return Math.round((inputCost + outputCost) * 10000) / 10000; // Round to 4 decimal places } catch (costError) { safeLog(null, `❌ Anthropic cost calculation error: ${costError.message}`); return 0; } } /** * Sanitize Anthropic request data with privacy protection */ function sanitizeAnthropicRequest(params) { try { if (!params) return {}; const sanitized = { model: params.model, max_tokens: params.max_tokens, temperature: params.temperature, top_p: params.top_p, top_k: params.top_k, stop_sequences: Array.isArray(params.stop_sequences) ? params.stop_sequences.length : 0, stream: params.stream }; // Handle system message if (params.system) { sanitized.system = typeof params.system === 'string' ? params.system.substring(0, 200) + (params.system.length > 200 ? '...' : '') : '[complex_system]'; } // Handle messages if (params.messages && Array.isArray(params.messages)) { sanitized.messages = params.messages.slice(0, 10).map(msg => ({ role: msg.role, content: typeof msg.content === 'string' ? msg.content.substring(0, 200) + (msg.content.length > 200 ? '...' : '') : Array.isArray(msg.content) ? `[${msg.content.length} content blocks]` : '[complex_content]' })); } return sanitized; } catch (sanitizeError) { return { error: 'sanitization_failed' }; } } /** * Sanitize Anthropic response data with privacy protection */ function sanitizeAnthropicResponse(response) { try { if (!response) return {}; const sanitized = { id: response.id, type: response.type, role: response.role, model: response.model, stop_reason: response.stop_reason, stop_sequence: response.stop_sequence, usage: response.usage }; // Handle content without exposing actual text if (response.content && Array.isArray(response.content)) { sanitized.content_blocks = response.content.length; sanitized.total_content_length = response.content.reduce((total, block) => { return total + (block.text?.length || 0); }, 0); } return sanitized; } catch (sanitizeError) { return { error: 'sanitization_failed' }; } } /** * Safe logging that never crashes */ function safeLog(guardaianClient, message) { try { if (guardaianClient?.options?.debug) { console.log(`🛡️ GuardAIan: ${message}`); } } catch (logError) { // Even logging can fail - do nothing to prevent infinite loops } } module.exports = { patchAnthropic };