import { circuitBreakerManager, CircuitBreakerOptions } from '../utils/circuit-breaker.js';
import { rateLimiters } from '../utils/rate-limiter.js';
import { structuredLogger } from '../utils/structured-logger.js';
/**
* Protected API client with circuit breaker and rate limiting
*/
export class ProtectedApiClient {
private serviceName: string;
private baseUrl: string;
private defaultHeaders: Record<string, string>;
private circuitBreakerOptions: Partial<CircuitBreakerOptions>;
constructor(config: {
serviceName: string;
baseUrl: string;
apiKey?: string;
defaultHeaders?: Record<string, string>;
circuitBreakerOptions?: Partial<CircuitBreakerOptions>;
}) {
this.serviceName = config.serviceName;
this.baseUrl = config.baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json',
...config.defaultHeaders
};
if (config.apiKey) {
this.defaultHeaders['Authorization'] = `Bearer ${config.apiKey}`;
}
this.circuitBreakerOptions = {
failureThreshold: 3,
resetTimeout: 60000, // 1 minute
timeout: 30000, // 30 seconds
...config.circuitBreakerOptions,
fallback: async () => {
throw new Error(`${this.serviceName} service is currently unavailable`);
}
};
}
/**
* Make a protected API request
*/
async request<T>(
endpoint: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
headers?: Record<string, string>;
correlationId?: string;
useFallback?: boolean;
} = {}
): Promise<T> {
const logger = structuredLogger.createApi(
this.serviceName,
endpoint,
options.correlationId
);
const breaker = circuitBreakerManager.getBreaker(
this.serviceName,
this.circuitBreakerOptions
);
return breaker.execute(async () => {
// Apply rate limiting
// Use appropriate rate limiter based on service
const limiter = this.serviceName.toLowerCase().includes('perplexity')
? rateLimiters.perplexity
: rateLimiters.fmp;
return limiter.executeWithRateLimit(async () => {
const url = `${this.baseUrl}${endpoint}`;
const method = options.method || 'GET';
logger.debug(`Making ${method} request`, { url });
const response = await fetch(url, {
method,
headers: {
...this.defaultHeaders,
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!response.ok) {
const error = new Error(`API request failed: ${response.status} ${response.statusText}`);
(error as any).status = response.status;
// Don't trip circuit for client errors
if (response.status >= 400 && response.status < 500) {
(error as any).skipCircuitBreaker = true;
}
throw error;
}
const data = await response.json();
logger.debug('Request successful', {
status: response.status,
dataSize: JSON.stringify(data).length
});
return data as T;
}, {
priority: 'normal',
timeout: this.circuitBreakerOptions.timeout
});
}, options.correlationId);
}
/**
* Check if the service is available
*/
isAvailable(): boolean {
const breaker = circuitBreakerManager.getBreaker(this.serviceName);
return breaker.isAvailable();
}
/**
* Get circuit breaker statistics
*/
getStats() {
const breaker = circuitBreakerManager.getBreaker(this.serviceName);
return breaker.getStats();
}
}
/**
* Create protected API clients for all external services
*/
export const protectedClients = {
perplexity: new ProtectedApiClient({
serviceName: 'Perplexity',
baseUrl: 'https://api.perplexity.ai',
apiKey: process.env.PERPLEXITY_API_KEY,
circuitBreakerOptions: {
failureThreshold: 3,
resetTimeout: 120000, // 2 minutes
errorFilter: (error) => {
// Don't trip for rate limits, handle them differently
return !(error as any).status || (error as any).status !== 429;
}
}
}),
financialModelingPrep: new ProtectedApiClient({
serviceName: 'FinancialModelingPrep',
baseUrl: 'https://financialmodelingprep.com/api/v3',
circuitBreakerOptions: {
failureThreshold: 5,
resetTimeout: 300000, // 5 minutes
}
}),
dutchGovAPI: new ProtectedApiClient({
serviceName: 'DutchGovAPI',
baseUrl: 'https://opendata.cbs.nl/api/v3',
circuitBreakerOptions: {
failureThreshold: 2,
resetTimeout: 600000, // 10 minutes
fallback: async () => {
// Return cached or default data
return {
data: [],
message: 'Using cached Dutch government data',
cached: true
};
}
}
})
};
/**
* Enhanced Sonar client with circuit breaker
*/
export class ProtectedSonarClient extends ProtectedApiClient {
constructor(apiKey: string) {
super({
serviceName: 'Perplexity-Sonar',
baseUrl: 'https://api.perplexity.ai',
apiKey,
defaultHeaders: {
'Content-Type': 'application/json',
},
circuitBreakerOptions: {
failureThreshold: 3,
resetTimeout: 60000,
timeout: 15000, // 15 seconds for Sonar
errorFilter: (error) => {
const status = (error as any).status;
// Don't trip for client errors or rate limits
return !status || (status >= 500);
},
fallback: async () => {
return {
content: 'Service temporarily unavailable. Using default benchmarks.',
citations: [],
fallback: true
};
}
}
});
}
async chat(messages: any[], options?: any) {
return this.request('/chat/completions', {
method: 'POST',
body: {
model: options?.model || 'sonar',
messages,
...options
},
correlationId: options?.correlationId
});
}
}
/**
* Health check endpoint data
*/
export function getApiHealth() {
return {
services: {
perplexity: protectedClients.perplexity.getStats(),
financialModelingPrep: protectedClients.financialModelingPrep.getStats(),
dutchGovAPI: protectedClients.dutchGovAPI.getStats()
},
overallHealth: circuitBreakerManager.isHealthy(),
timestamp: new Date().toISOString()
};
}