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