228 lines
7.8 KiB
JavaScript
228 lines
7.8 KiB
JavaScript
// QUIC-enabled Proxy for Anthropic API
|
|
// Optional QUIC transport with automatic HTTP/2 fallback
|
|
import { QuicClient, QuicConnectionPool } from '../transport/quic.js';
|
|
import { logger } from '../utils/logger.js';
|
|
import { AnthropicToOpenRouterProxy } from './anthropic-to-openrouter.js';
|
|
export class QuicEnabledProxy extends AnthropicToOpenRouterProxy {
|
|
quicClient;
|
|
quicPool;
|
|
transport;
|
|
quicEnabled;
|
|
fallbackToHttp2;
|
|
constructor(config) {
|
|
super({
|
|
openrouterApiKey: config.openrouterApiKey,
|
|
openrouterBaseUrl: config.openrouterBaseUrl,
|
|
defaultModel: config.defaultModel
|
|
});
|
|
this.transport = config.transport || 'auto';
|
|
this.quicEnabled = config.enableQuic ?? this.checkQuicFeatureFlag();
|
|
this.fallbackToHttp2 = config.fallbackToHttp2 ?? true;
|
|
if (this.quicEnabled) {
|
|
this.initializeQuic(config.quic || {});
|
|
}
|
|
}
|
|
/**
|
|
* Check if QUIC is enabled via environment variable
|
|
*/
|
|
checkQuicFeatureFlag() {
|
|
const flag = process.env.AGENTIC_FLOW_ENABLE_QUIC;
|
|
return flag === 'true' || flag === '1';
|
|
}
|
|
/**
|
|
* Initialize QUIC client and connection pool
|
|
*/
|
|
async initializeQuic(quicConfig) {
|
|
try {
|
|
logger.info('Initializing QUIC transport...', { config: quicConfig });
|
|
this.quicClient = new QuicClient(quicConfig);
|
|
await this.quicClient.initialize();
|
|
this.quicPool = new QuicConnectionPool(this.quicClient, 20);
|
|
logger.info('QUIC transport initialized successfully');
|
|
}
|
|
catch (error) {
|
|
logger.error('Failed to initialize QUIC transport', { error });
|
|
if (this.fallbackToHttp2) {
|
|
logger.warn('Falling back to HTTP/2 transport');
|
|
this.quicEnabled = false;
|
|
}
|
|
else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Select transport protocol based on configuration and availability
|
|
*/
|
|
selectTransport() {
|
|
if (this.transport === 'quic' && this.quicEnabled) {
|
|
return 'quic';
|
|
}
|
|
if (this.transport === 'http2') {
|
|
return 'http2';
|
|
}
|
|
// Auto mode: prefer QUIC if available, fallback to HTTP/2
|
|
if (this.transport === 'auto') {
|
|
return this.quicEnabled ? 'quic' : 'http2';
|
|
}
|
|
return 'http2';
|
|
}
|
|
/**
|
|
* Send request using selected transport
|
|
*/
|
|
async sendRequest(url, options) {
|
|
const selectedTransport = this.selectTransport();
|
|
logger.debug('Sending request', {
|
|
transport: selectedTransport,
|
|
url,
|
|
method: options.method
|
|
});
|
|
if (selectedTransport === 'quic') {
|
|
return this.sendQuicRequest(url, options);
|
|
}
|
|
else {
|
|
return this.sendHttp2Request(url, options);
|
|
}
|
|
}
|
|
/**
|
|
* Send request over QUIC
|
|
*/
|
|
async sendQuicRequest(url, options) {
|
|
if (!this.quicClient || !this.quicPool) {
|
|
throw new Error('QUIC client not initialized');
|
|
}
|
|
try {
|
|
const urlObj = new URL(url);
|
|
const connection = await this.quicPool.getConnection(urlObj.hostname, parseInt(urlObj.port) || 443);
|
|
logger.debug('Using QUIC connection', {
|
|
connectionId: connection.id,
|
|
url
|
|
});
|
|
// Prepare headers
|
|
const headers = {};
|
|
if (options.headers) {
|
|
const headerEntries = options.headers instanceof Headers
|
|
? Array.from(options.headers.entries())
|
|
: Object.entries(options.headers);
|
|
for (const [key, value] of headerEntries) {
|
|
headers[key] = value;
|
|
}
|
|
}
|
|
// Convert body to Uint8Array
|
|
let body;
|
|
if (options.body) {
|
|
if (typeof options.body === 'string') {
|
|
body = new TextEncoder().encode(options.body);
|
|
}
|
|
else if (options.body instanceof Uint8Array) {
|
|
body = options.body;
|
|
}
|
|
else {
|
|
body = new TextEncoder().encode(JSON.stringify(options.body));
|
|
}
|
|
}
|
|
// Send HTTP/3 request over QUIC
|
|
const response = await this.quicClient.sendRequest(connection.id, options.method || 'GET', urlObj.pathname + urlObj.search, headers, body);
|
|
logger.info('QUIC request completed', {
|
|
status: response.status,
|
|
bytes: response.body.length
|
|
});
|
|
// Convert to fetch Response
|
|
const responseText = new TextDecoder().decode(response.body);
|
|
return new Response(responseText, {
|
|
status: response.status,
|
|
headers: new Headers(response.headers)
|
|
});
|
|
}
|
|
catch (error) {
|
|
logger.error('QUIC request failed', { error, url });
|
|
if (this.fallbackToHttp2) {
|
|
logger.warn('Falling back to HTTP/2 for this request');
|
|
return this.sendHttp2Request(url, options);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* Send request over HTTP/2 (standard fetch)
|
|
*/
|
|
async sendHttp2Request(url, options) {
|
|
logger.debug('Using HTTP/2 transport', { url });
|
|
return fetch(url, options);
|
|
}
|
|
/**
|
|
* Get transport statistics
|
|
*/
|
|
getTransportStats() {
|
|
if (this.quicClient) {
|
|
return {
|
|
transport: this.selectTransport(),
|
|
quicEnabled: this.quicEnabled,
|
|
quicStats: this.quicClient.getStats()
|
|
};
|
|
}
|
|
return {
|
|
transport: 'http2',
|
|
quicEnabled: false
|
|
};
|
|
}
|
|
/**
|
|
* Shutdown and cleanup
|
|
*/
|
|
async shutdown() {
|
|
if (this.quicPool) {
|
|
await this.quicPool.clear();
|
|
}
|
|
if (this.quicClient) {
|
|
await this.quicClient.shutdown();
|
|
}
|
|
logger.info('QUIC proxy shutdown complete');
|
|
}
|
|
}
|
|
/**
|
|
* Create QUIC-enabled proxy with configuration
|
|
*/
|
|
export function createQuicProxy(config) {
|
|
const proxy = new QuicEnabledProxy(config);
|
|
logger.info('QUIC proxy created', {
|
|
transport: config.transport || 'auto',
|
|
quicEnabled: config.enableQuic ?? (process.env.AGENTIC_FLOW_ENABLE_QUIC === 'true'),
|
|
fallbackEnabled: config.fallbackToHttp2 ?? true
|
|
});
|
|
return proxy;
|
|
}
|
|
// CLI entry point for QUIC proxy
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const port = parseInt(process.env.PORT || '3000');
|
|
const openrouterApiKey = process.env.OPENROUTER_API_KEY;
|
|
if (!openrouterApiKey) {
|
|
console.error('❌ Error: OPENROUTER_API_KEY environment variable required');
|
|
process.exit(1);
|
|
}
|
|
const proxy = createQuicProxy({
|
|
openrouterApiKey,
|
|
openrouterBaseUrl: process.env.ANTHROPIC_PROXY_BASE_URL,
|
|
defaultModel: process.env.COMPLETION_MODEL || process.env.REASONING_MODEL,
|
|
transport: process.env.TRANSPORT || 'auto',
|
|
enableQuic: process.env.AGENTIC_FLOW_ENABLE_QUIC === 'true',
|
|
quic: {
|
|
port: parseInt(process.env.QUIC_PORT || '4433'),
|
|
serverHost: process.env.QUIC_HOST || 'localhost',
|
|
certPath: process.env.QUIC_CERT_PATH,
|
|
keyPath: process.env.QUIC_KEY_PATH
|
|
}
|
|
});
|
|
proxy.start(port);
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', async () => {
|
|
logger.info('Received SIGTERM, shutting down gracefully...');
|
|
await proxy.shutdown();
|
|
process.exit(0);
|
|
});
|
|
process.on('SIGINT', async () => {
|
|
logger.info('Received SIGINT, shutting down gracefully...');
|
|
await proxy.shutdown();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
//# sourceMappingURL=quic-proxy.js.map
|