@guardaian/sdk
Version:
Zero-friction AI governance and monitoring SDK for Node.js applications
392 lines (340 loc) • 13.3 kB
JavaScript
/**
* 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
};