UNPKG

@guardaian/sdk

Version:

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

392 lines (340 loc) 13.3 kB
/** * AWS Bedrock API Interceptor - Bulletproof Production Version * * CRITICAL GUARANTEES: * 1. Customer's Bedrock 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. Supports all Bedrock model providers (Claude, Llama, Titan, etc.) */ function patchBedrock(BedrockRuntime, guardaianClient) { // Early validation with error protection if (!BedrockRuntime) { safeLog(guardaianClient, '⚠️ AWS Bedrock Runtime not provided - skipping patch (non-blocking)'); return; } // Prevent double-patching if (BedrockRuntime.__guardaianPatched) { safeLog(guardaianClient, '⚠️ AWS Bedrock already patched - skipping'); return; } try { safeLog(guardaianClient, '🔧 Patching AWS Bedrock SDK with bulletproof protection...'); // Patch BedrockRuntimeClient if (BedrockRuntime.BedrockRuntimeClient) { patchBedrockRuntimeClient(BedrockRuntime.BedrockRuntimeClient, guardaianClient); } // Mark as patched BedrockRuntime.__guardaianPatched = true; safeLog(guardaianClient, '✅ AWS Bedrock SDK patched successfully with failsafe protection'); } catch (patchError) { // Even patching errors should not break customer code safeLog(guardaianClient, `⚠️ AWS Bedrock patching failed (non-blocking): ${patchError.message}`); } } /** * Patch AWS Bedrock Runtime Client with bulletproof error handling */ function patchBedrockRuntimeClient(BedrockRuntimeClient, guardaianClient) { try { const originalInvokeModel = BedrockRuntimeClient.prototype.invokeModel; if (!originalInvokeModel) { safeLog(guardaianClient, '⚠️ BedrockRuntimeClient.invokeModel not found - skipping patch'); return; } BedrockRuntimeClient.prototype.invokeModel = async function(params, options) { const startTime = Date.now(); // CRITICAL: Always call original Bedrock API first // Customer's AI call must NEVER depend on GuardAIan let response, originalError; try { // Execute customer's actual Bedrock API call response = await originalInvokeModel.call(this, params, options); } catch (error) { // Store the original error to re-throw later originalError = error; } // CRITICAL: Track usage in completely isolated context setImmediate(() => { try { trackBedrockUsage(params, response, originalError, startTime, guardaianClient); } catch (trackingError) { // Tracking errors are completely isolated from customer code safeLog(guardaianClient, `❌ Bedrock tracking error (isolated): ${trackingError.message}`); } }); // CRITICAL: Always return original result or throw original error if (originalError) { throw originalError; } return response; }; } catch (patchError) { safeLog(guardaianClient, `❌ Failed to patch Bedrock runtime client: ${patchError.message}`); } } /** * Track Bedrock usage with complete error isolation */ async function trackBedrockUsage(params, response, error, startTime, guardaianClient) { try { const endTime = Date.now(); const duration = endTime - startTime; const modelId = params.modelId || 'unknown-model'; if (response) { // Successful request tracking const requestBody = parseBedrockRequestBody(params.body, modelId); const responseBody = parseBedrockResponseBody(response.body, modelId); const usage = extractBedrockUsage(requestBody, responseBody, modelId); const cost = calculateBedrockCost(modelId, usage); // Fire-and-forget tracking call guardaianClient.track({ service: 'aws-bedrock', model: modelId, operation: 'invokeModel', inputTokens: usage.inputTokens || 0, outputTokens: usage.outputTokens || 0, totalTokens: (usage.inputTokens || 0) + (usage.outputTokens || 0), cost: cost, duration: duration, requestData: sanitizeBedrockRequest(params, requestBody), responseData: sanitizeBedrockResponse(response, responseBody), metadata: { success: true, provider: extractProviderFromModelId(modelId), contentType: params.contentType || 'application/json' } }).catch(trackingError => { // Even the track() call errors are isolated safeLog(guardaianClient, `❌ Bedrock track call failed (isolated): ${trackingError.message}`); }); safeLog(guardaianClient, `✅ Tracked Bedrock call: ${modelId} (${(usage.inputTokens || 0) + (usage.outputTokens || 0)} tokens, $${cost.toFixed(6)})`); } else if (error) { // Failed request tracking guardaianClient.track({ service: 'aws-bedrock', model: modelId, operation: 'invokeModel', inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, duration: duration, requestData: sanitizeBedrockRequest(params, {}), responseData: { error: true, status: error.$metadata?.httpStatusCode || 'unknown', errorCode: error.name || 'unknown', message: error.message?.substring(0, 200) || 'Unknown error' }, metadata: { success: false, errorType: error.constructor?.name || 'UnknownError', provider: extractProviderFromModelId(modelId) } }).catch(trackingError => { safeLog(guardaianClient, `❌ Bedrock error tracking failed (isolated): ${trackingError.message}`); }); safeLog(guardaianClient, `📊 Tracked Bedrock error: ${modelId} - ${error.message}`); } } catch (trackingError) { // Ultimate safety net safeLog(guardaianClient, `❌ Bedrock usage tracking completely failed (isolated): ${trackingError.message}`); } } /** * Parse Bedrock request body with error protection */ function parseBedrockRequestBody(body, modelId) { try { if (body instanceof Uint8Array) { body = new TextDecoder().decode(body); } if (typeof body === 'string') { return JSON.parse(body); } return body || {}; } catch (parseError) { return { raw: body?.toString?.() || '[unparseable]', parseError: parseError.message }; } } /** * Parse Bedrock response body with error protection */ function parseBedrockResponseBody(body, modelId) { try { if (body instanceof Uint8Array) { body = new TextDecoder().decode(body); } if (typeof body === 'string') { return JSON.parse(body); } return body || {}; } catch (parseError) { return { raw: body?.toString?.() || '[unparseable]', parseError: parseError.message }; } } /** * Extract usage metrics from request/response with error protection */ function extractBedrockUsage(requestBody, responseBody, modelId) { try { const provider = extractProviderFromModelId(modelId); switch (provider) { case 'anthropic': return { inputTokens: responseBody.usage?.input_tokens || estimateTokens(requestBody.messages?.[0]?.content || ''), outputTokens: responseBody.usage?.output_tokens || estimateTokens(responseBody.content?.[0]?.text || '') }; case 'amazon': return { inputTokens: responseBody.inputTextTokenCount || estimateTokens(requestBody.inputText || ''), outputTokens: responseBody.results?.[0]?.tokenCount || estimateTokens(responseBody.results?.[0]?.outputText || '') }; case 'meta': return { inputTokens: estimateTokens(requestBody.prompt || ''), outputTokens: estimateTokens(responseBody.generation || '') }; case 'cohere': return { inputTokens: responseBody.meta?.tokens?.input_tokens || estimateTokens(requestBody.message || ''), outputTokens: responseBody.meta?.tokens?.output_tokens || estimateTokens(responseBody.text || '') }; case 'ai21': return { inputTokens: estimateTokens(requestBody.prompt || ''), outputTokens: responseBody.completions?.[0]?.data?.tokens?.length || estimateTokens(responseBody.completions?.[0]?.data?.text || '') }; default: return { inputTokens: estimateTokens(JSON.stringify(requestBody).substring(0, 1000)), outputTokens: estimateTokens(JSON.stringify(responseBody).substring(0, 1000)) }; } } catch (extractError) { return { inputTokens: 0, outputTokens: 0 }; } } /** * Extract provider from model ID with error protection */ function extractProviderFromModelId(modelId) { try { if (!modelId || typeof modelId !== 'string') return 'unknown'; if (modelId.includes('anthropic')) return 'anthropic'; if (modelId.includes('amazon')) return 'amazon'; if (modelId.includes('meta')) return 'meta'; if (modelId.includes('cohere')) return 'cohere'; if (modelId.includes('ai21')) return 'ai21'; if (modelId.includes('stability')) return 'stability'; return 'unknown'; } catch (providerError) { return 'unknown'; } } /** * Estimate tokens from text with error protection */ function estimateTokens(text) { try { if (!text || typeof text !== 'string') return 0; return Math.ceil(text.length / 4); // Rough estimate: 1 token ≈ 4 characters } catch (estimateError) { return 0; } } /** * Calculate cost for Bedrock API calls with error protection */ function calculateBedrockCost(modelId, usage) { try { // Bedrock pricing varies by model and region (using us-east-1 rates) const bedrockPricing = { // Anthropic Claude 'anthropic.claude-3-5-sonnet-20241022-v2:0': { input: 0.003, output: 0.015 }, 'anthropic.claude-3-5-haiku-20241022-v1:0': { input: 0.001, output: 0.005 }, 'anthropic.claude-3-opus-20240229-v1:0': { input: 0.015, output: 0.075 }, 'anthropic.claude-3-sonnet-20240229-v1:0': { input: 0.003, output: 0.015 }, 'anthropic.claude-3-haiku-20240307-v1:0': { input: 0.00025, output: 0.00125 }, // Amazon Titan 'amazon.titan-text-express-v1': { input: 0.0008, output: 0.0016 }, 'amazon.titan-text-lite-v1': { input: 0.0003, output: 0.0004 }, 'amazon.titan-embed-text-v1': { input: 0.0001, output: 0 }, // Meta Llama 'meta.llama2-13b-chat-v1': { input: 0.00075, output: 0.001 }, 'meta.llama2-70b-chat-v1': { input: 0.00195, output: 0.00256 }, // Cohere 'cohere.command-text-v14': { input: 0.0015, output: 0.002 }, 'cohere.command-light-text-v14': { input: 0.0003, output: 0.0006 }, // AI21 Labs 'ai21.j2-mid-v1': { input: 0.0125, output: 0.0125 }, 'ai21.j2-ultra-v1': { input: 0.0188, output: 0.0188 } }; // Default pricing if model not found const defaultPricing = { input: 0.001, output: 0.002 }; const modelPricing = bedrockPricing[modelId] || defaultPricing; const inputCost = (usage.inputTokens || 0) * modelPricing.input / 1000; const outputCost = (usage.outputTokens || 0) * modelPricing.output / 1000; return Math.round((inputCost + outputCost) * 10000) / 10000; } catch (costError) { safeLog(null, `❌ Bedrock cost calculation error: ${costError.message}`); return 0; } } /** * Sanitize Bedrock request data with privacy protection */ function sanitizeBedrockRequest(params, requestBody) { try { return { modelId: params.modelId, contentType: params.contentType || 'application/json', hasMessages: !!requestBody.messages, messageCount: requestBody.messages?.length || 0, hasPrompt: !!requestBody.prompt, promptLength: requestBody.prompt?.length || 0, temperature: requestBody.temperature, maxTokens: requestBody.max_tokens || requestBody.maxTokenCount, topP: requestBody.top_p || requestBody.topP, topK: requestBody.top_k || requestBody.topK, provider: extractProviderFromModelId(params.modelId) }; } catch (sanitizeError) { return { error: 'sanitization_failed' }; } } /** * Sanitize Bedrock response data with privacy protection */ function sanitizeBedrockResponse(response, responseBody) { try { return { modelId: response.ResponseMetadata?.RequestId || 'unknown', contentType: response.contentType, hasContent: !!responseBody.content, contentLength: responseBody.content?.[0]?.text?.length || 0, hasCompletion: !!responseBody.completions, completionCount: responseBody.completions?.length || 0, stopReason: responseBody.stop_reason || responseBody.completions?.[0]?.finishReason, hasUsage: !!responseBody.usage }; } 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 } } module.exports = { patchBedrock };