agentic-flow 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +246 -0
  2. package/dist/proxy/adaptive-proxy.js +224 -0
  3. package/dist/proxy/anthropic-to-gemini.js +2 -2
  4. package/dist/proxy/http2-proxy-optimized.js +191 -0
  5. package/dist/proxy/http2-proxy.js +381 -0
  6. package/dist/proxy/http3-proxy-old.js +331 -0
  7. package/dist/proxy/http3-proxy.js +51 -0
  8. package/dist/proxy/websocket-proxy.js +406 -0
  9. package/dist/utils/auth.js +52 -0
  10. package/dist/utils/compression-middleware.js +149 -0
  11. package/dist/utils/connection-pool.js +184 -0
  12. package/dist/utils/rate-limiter.js +48 -0
  13. package/dist/utils/response-cache.js +211 -0
  14. package/dist/utils/streaming-optimizer.js +141 -0
  15. package/docs/.claude-flow/metrics/performance.json +3 -3
  16. package/docs/.claude-flow/metrics/task-metrics.json +3 -3
  17. package/docs/ISSUE-55-VALIDATION.md +152 -0
  18. package/docs/OPTIMIZATIONS.md +460 -0
  19. package/docs/README.md +217 -0
  20. package/docs/issues/ISSUE-xenova-transformers-dependency.md +380 -0
  21. package/package.json +1 -1
  22. package/scripts/claude +31 -0
  23. package/validation/test-gemini-exclusiveMinimum-fix.ts +142 -0
  24. package/validation/validate-v1.10.0-docker.sh +296 -0
  25. package/wasm/reasoningbank/reasoningbank_wasm_bg.js +2 -2
  26. package/wasm/reasoningbank/reasoningbank_wasm_bg.wasm +0 -0
  27. package/docs/INDEX.md +0 -279
  28. package/docs/guides/.claude-flow/metrics/agent-metrics.json +0 -1
  29. package/docs/guides/.claude-flow/metrics/performance.json +0 -9
  30. package/docs/guides/.claude-flow/metrics/task-metrics.json +0 -10
  31. package/docs/router/.claude-flow/metrics/agent-metrics.json +0 -1
  32. package/docs/router/.claude-flow/metrics/performance.json +0 -9
  33. package/docs/router/.claude-flow/metrics/task-metrics.json +0 -10
  34. /package/docs/{TEST-V1.7.8.Dockerfile → docker-tests/TEST-V1.7.8.Dockerfile} +0 -0
  35. /package/docs/{TEST-V1.7.9-NODE20.Dockerfile → docker-tests/TEST-V1.7.9-NODE20.Dockerfile} +0 -0
  36. /package/docs/{TEST-V1.7.9.Dockerfile → docker-tests/TEST-V1.7.9.Dockerfile} +0 -0
  37. /package/docs/{v1.7.1-QUICK-START.md → guides/QUICK-START-v1.7.1.md} +0 -0
  38. /package/docs/{INTEGRATION-COMPLETE.md → integration-docs/INTEGRATION-COMPLETE.md} +0 -0
  39. /package/docs/{LANDING-PAGE-PROVIDER-CONTENT.md → providers/LANDING-PAGE-PROVIDER-CONTENT.md} +0 -0
  40. /package/docs/{PROVIDER-FALLBACK-GUIDE.md → providers/PROVIDER-FALLBACK-GUIDE.md} +0 -0
  41. /package/docs/{PROVIDER-FALLBACK-SUMMARY.md → providers/PROVIDER-FALLBACK-SUMMARY.md} +0 -0
  42. /package/docs/{QUIC_FINAL_STATUS.md → quic/QUIC_FINAL_STATUS.md} +0 -0
  43. /package/docs/{README_QUIC_PHASE1.md → quic/README_QUIC_PHASE1.md} +0 -0
  44. /package/docs/{AGENTDB_TESTING.md → testing/AGENTDB_TESTING.md} +0 -0
@@ -0,0 +1,406 @@
1
+ /**
2
+ * WebSocket Proxy for LLM Streaming
3
+ *
4
+ * Features:
5
+ * - Bidirectional: Full-duplex communication
6
+ * - Mobile-friendly: Better for unstable connections
7
+ * - Lower overhead: No HTTP headers per message
8
+ * - Reconnection: Automatic retry on disconnect
9
+ * - Universal support: Works everywhere (browsers, mobile, desktop)
10
+ *
11
+ * Use Case: Fallback for unreliable connections (mobile, poor WiFi)
12
+ */
13
+ import { WebSocketServer } from 'ws';
14
+ import { createServer } from 'http';
15
+ import { logger } from '../utils/logger.js';
16
+ export class WebSocketProxy {
17
+ wss;
18
+ server;
19
+ config;
20
+ clients = new Map();
21
+ pingInterval;
22
+ activeConnections = 0;
23
+ constructor(config) {
24
+ this.config = config;
25
+ this.server = createServer();
26
+ this.wss = new WebSocketServer({ server: this.server });
27
+ this.setupHandlers();
28
+ this.setupHeartbeat();
29
+ logger.info('WebSocket proxy created', {
30
+ port: config.port,
31
+ pingInterval: config.pingInterval || 30000
32
+ });
33
+ }
34
+ setupHandlers() {
35
+ this.wss.on('connection', (ws, req) => {
36
+ const clientIp = req.socket.remoteAddress;
37
+ // Check connection limit (DoS protection)
38
+ const maxConnections = this.config.maxConnections || 1000;
39
+ if (this.activeConnections >= maxConnections) {
40
+ logger.warn('Connection limit reached', {
41
+ activeConnections: this.activeConnections,
42
+ maxConnections
43
+ });
44
+ ws.close(1008, 'Server at capacity');
45
+ return;
46
+ }
47
+ this.activeConnections++;
48
+ logger.info('WebSocket client connected', {
49
+ clientIp,
50
+ activeConnections: this.activeConnections
51
+ });
52
+ // Initialize client state
53
+ this.clients.set(ws, {
54
+ ws,
55
+ isAlive: true,
56
+ lastPing: new Date(),
57
+ requests: 0
58
+ });
59
+ // Handle incoming messages
60
+ ws.on('message', async (data) => {
61
+ const client = this.clients.get(ws);
62
+ if (!client)
63
+ return;
64
+ client.requests++;
65
+ try {
66
+ const message = JSON.parse(data.toString());
67
+ logger.debug('WebSocket message received', { type: message.type });
68
+ if (message.type === 'ping') {
69
+ // Respond to ping
70
+ ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
71
+ client.isAlive = true;
72
+ client.lastPing = new Date();
73
+ }
74
+ else if (message.type === 'streaming_request') {
75
+ // Handle LLM streaming request
76
+ await this.handleStreamingRequest(ws, message.data);
77
+ }
78
+ else if (message.type === 'non_streaming_request') {
79
+ // Handle non-streaming request
80
+ await this.handleNonStreamingRequest(ws, message.data);
81
+ }
82
+ else {
83
+ ws.send(JSON.stringify({
84
+ type: 'error',
85
+ error: `Unknown message type: ${message.type}`
86
+ }));
87
+ }
88
+ }
89
+ catch (error) {
90
+ logger.error('WebSocket message error', { error: error.message });
91
+ ws.send(JSON.stringify({
92
+ type: 'error',
93
+ error: error.message
94
+ }));
95
+ }
96
+ });
97
+ // Handle pong responses
98
+ ws.on('pong', () => {
99
+ const client = this.clients.get(ws);
100
+ if (client) {
101
+ client.isAlive = true;
102
+ client.lastPing = new Date();
103
+ }
104
+ });
105
+ // Set connection timeout
106
+ const connectionTimeout = this.config.connectionTimeout || 300000; // 5 minutes
107
+ const timeoutHandle = setTimeout(() => {
108
+ logger.warn('Connection timeout', { clientIp });
109
+ ws.close(1000, 'Connection timeout');
110
+ }, connectionTimeout);
111
+ // Handle close
112
+ ws.on('close', () => {
113
+ logger.info('WebSocket client disconnected', { clientIp });
114
+ this.clients.delete(ws);
115
+ this.activeConnections--;
116
+ clearTimeout(timeoutHandle);
117
+ });
118
+ // Handle errors
119
+ ws.on('error', (error) => {
120
+ logger.error('WebSocket error', { clientIp, error: error.message });
121
+ this.clients.delete(ws);
122
+ this.activeConnections--;
123
+ });
124
+ // Send initial handshake
125
+ ws.send(JSON.stringify({
126
+ type: 'connected',
127
+ protocols: ['anthropic-messages-v1'],
128
+ features: ['streaming', 'non-streaming', 'ping-pong']
129
+ }));
130
+ });
131
+ this.wss.on('error', (error) => {
132
+ logger.error('WebSocket server error', { error: error.message });
133
+ });
134
+ }
135
+ setupHeartbeat() {
136
+ const interval = this.config.pingInterval || 30000; // 30 seconds
137
+ const timeout = this.config.pingTimeout || 60000; // 60 seconds
138
+ this.pingInterval = setInterval(() => {
139
+ const now = Date.now();
140
+ this.clients.forEach((client, ws) => {
141
+ // Check if client responded to last ping
142
+ if (!client.isAlive) {
143
+ const timeSinceLastPing = now - client.lastPing.getTime();
144
+ if (timeSinceLastPing > timeout) {
145
+ logger.warn('Client did not respond to ping, terminating', {
146
+ timeSinceLastPing
147
+ });
148
+ ws.terminate();
149
+ this.clients.delete(ws);
150
+ return;
151
+ }
152
+ }
153
+ // Send ping
154
+ client.isAlive = false;
155
+ ws.ping();
156
+ });
157
+ }, interval);
158
+ }
159
+ async handleStreamingRequest(ws, anthropicReq) {
160
+ try {
161
+ logger.info('WebSocket streaming request', {
162
+ model: anthropicReq.model,
163
+ messageCount: anthropicReq.messages?.length
164
+ });
165
+ // Convert to Gemini format
166
+ const geminiReq = this.convertAnthropicToGemini(anthropicReq);
167
+ // Send streaming start event
168
+ ws.send(JSON.stringify({
169
+ type: 'message_start',
170
+ message: {
171
+ id: `msg_${Date.now()}`,
172
+ role: 'assistant',
173
+ model: anthropicReq.model || 'gemini-2.0-flash-exp'
174
+ }
175
+ }));
176
+ // Forward to Gemini
177
+ const geminiBaseUrl = this.config.geminiBaseUrl || 'https://generativelanguage.googleapis.com/v1beta';
178
+ const url = `${geminiBaseUrl}/models/gemini-2.0-flash-exp:streamGenerateContent?key=${this.config.geminiApiKey}&alt=sse`;
179
+ const response = await fetch(url, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify(geminiReq)
183
+ });
184
+ if (!response.ok) {
185
+ const error = await response.text();
186
+ throw new Error(`Gemini API error: ${error}`);
187
+ }
188
+ const reader = response.body?.getReader();
189
+ if (!reader) {
190
+ throw new Error('No response body');
191
+ }
192
+ const decoder = new TextDecoder();
193
+ let chunkCount = 0;
194
+ while (true) {
195
+ const { done, value } = await reader.read();
196
+ if (done)
197
+ break;
198
+ const chunk = decoder.decode(value);
199
+ chunkCount++;
200
+ // Parse Gemini SSE and send as WebSocket messages
201
+ const lines = chunk.split('\n').filter(line => line.trim());
202
+ for (const line of lines) {
203
+ if (line.startsWith('data: ')) {
204
+ try {
205
+ const jsonStr = line.substring(6);
206
+ const parsed = JSON.parse(jsonStr);
207
+ const candidate = parsed.candidates?.[0];
208
+ const text = candidate?.content?.parts?.[0]?.text;
209
+ if (text) {
210
+ // Send text delta
211
+ ws.send(JSON.stringify({
212
+ type: 'content_block_delta',
213
+ delta: { type: 'text_delta', text }
214
+ }));
215
+ }
216
+ if (candidate?.finishReason) {
217
+ // Send completion
218
+ ws.send(JSON.stringify({
219
+ type: 'message_stop',
220
+ stop_reason: 'end_turn'
221
+ }));
222
+ }
223
+ }
224
+ catch (e) {
225
+ logger.debug('Failed to parse stream chunk', { line });
226
+ }
227
+ }
228
+ }
229
+ }
230
+ logger.info('WebSocket stream complete', { totalChunks: chunkCount });
231
+ }
232
+ catch (error) {
233
+ logger.error('WebSocket streaming error', { error: error.message });
234
+ ws.send(JSON.stringify({
235
+ type: 'error',
236
+ error: error.message
237
+ }));
238
+ }
239
+ }
240
+ async handleNonStreamingRequest(ws, anthropicReq) {
241
+ try {
242
+ logger.info('WebSocket non-streaming request', {
243
+ model: anthropicReq.model,
244
+ messageCount: anthropicReq.messages?.length
245
+ });
246
+ // Convert to Gemini format
247
+ const geminiReq = this.convertAnthropicToGemini(anthropicReq);
248
+ // Forward to Gemini
249
+ const geminiBaseUrl = this.config.geminiBaseUrl || 'https://generativelanguage.googleapis.com/v1beta';
250
+ const url = `${geminiBaseUrl}/models/gemini-2.0-flash-exp:generateContent?key=${this.config.geminiApiKey}`;
251
+ const response = await fetch(url, {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify(geminiReq)
255
+ });
256
+ if (!response.ok) {
257
+ const error = await response.text();
258
+ throw new Error(`Gemini API error: ${error}`);
259
+ }
260
+ const geminiRes = await response.json();
261
+ const anthropicRes = this.convertGeminiToAnthropic(geminiRes);
262
+ // Send complete response
263
+ ws.send(JSON.stringify({
264
+ type: 'message_complete',
265
+ message: anthropicRes
266
+ }));
267
+ }
268
+ catch (error) {
269
+ logger.error('WebSocket non-streaming error', { error: error.message });
270
+ ws.send(JSON.stringify({
271
+ type: 'error',
272
+ error: error.message
273
+ }));
274
+ }
275
+ }
276
+ convertAnthropicToGemini(anthropicReq) {
277
+ const contents = [];
278
+ let systemPrefix = '';
279
+ if (anthropicReq.system) {
280
+ systemPrefix = `System: ${anthropicReq.system}\n\n`;
281
+ }
282
+ for (let i = 0; i < anthropicReq.messages.length; i++) {
283
+ const msg = anthropicReq.messages[i];
284
+ let text;
285
+ if (typeof msg.content === 'string') {
286
+ text = msg.content;
287
+ }
288
+ else if (Array.isArray(msg.content)) {
289
+ text = msg.content
290
+ .filter((block) => block.type === 'text')
291
+ .map((block) => block.text)
292
+ .join('\n');
293
+ }
294
+ else {
295
+ text = '';
296
+ }
297
+ if (i === 0 && msg.role === 'user' && systemPrefix) {
298
+ text = systemPrefix + text;
299
+ }
300
+ contents.push({
301
+ role: msg.role === 'assistant' ? 'model' : 'user',
302
+ parts: [{ text }]
303
+ });
304
+ }
305
+ const geminiReq = { contents };
306
+ if (anthropicReq.temperature !== undefined || anthropicReq.max_tokens !== undefined) {
307
+ geminiReq.generationConfig = {};
308
+ if (anthropicReq.temperature !== undefined) {
309
+ geminiReq.generationConfig.temperature = anthropicReq.temperature;
310
+ }
311
+ if (anthropicReq.max_tokens !== undefined) {
312
+ geminiReq.generationConfig.maxOutputTokens = anthropicReq.max_tokens;
313
+ }
314
+ }
315
+ return geminiReq;
316
+ }
317
+ convertGeminiToAnthropic(geminiRes) {
318
+ const candidate = geminiRes.candidates?.[0];
319
+ if (!candidate) {
320
+ throw new Error('No candidates in Gemini response');
321
+ }
322
+ const content = candidate.content;
323
+ const parts = content?.parts || [];
324
+ let rawText = '';
325
+ for (const part of parts) {
326
+ if (part.text) {
327
+ rawText += part.text;
328
+ }
329
+ }
330
+ return {
331
+ id: `msg_${Date.now()}`,
332
+ type: 'message',
333
+ role: 'assistant',
334
+ model: 'gemini-2.0-flash-exp',
335
+ content: [
336
+ {
337
+ type: 'text',
338
+ text: rawText
339
+ }
340
+ ],
341
+ stop_reason: 'end_turn',
342
+ usage: {
343
+ input_tokens: geminiRes.usageMetadata?.promptTokenCount || 0,
344
+ output_tokens: geminiRes.usageMetadata?.candidatesTokenCount || 0
345
+ }
346
+ };
347
+ }
348
+ start() {
349
+ return new Promise((resolve) => {
350
+ this.server.listen(this.config.port, () => {
351
+ logger.info('WebSocket proxy started', {
352
+ port: this.config.port,
353
+ url: `ws://localhost:${this.config.port}`
354
+ });
355
+ console.log(`\n✅ WebSocket Proxy running at ws://localhost:${this.config.port}`);
356
+ console.log(` Protocol: WebSocket (fallback for unreliable connections)`);
357
+ console.log(` Features: Bidirectional, Mobile-friendly, Auto-reconnect\n`);
358
+ resolve();
359
+ });
360
+ });
361
+ }
362
+ stop() {
363
+ return new Promise((resolve) => {
364
+ // Clear ping interval
365
+ if (this.pingInterval) {
366
+ clearInterval(this.pingInterval);
367
+ }
368
+ // Close all client connections
369
+ this.clients.forEach((client, ws) => {
370
+ ws.close(1000, 'Server shutting down');
371
+ });
372
+ this.clients.clear();
373
+ // Close WebSocket server
374
+ this.wss.close(() => {
375
+ // Close HTTP server
376
+ this.server.close(() => {
377
+ logger.info('WebSocket proxy stopped');
378
+ resolve();
379
+ });
380
+ });
381
+ });
382
+ }
383
+ }
384
+ // CLI entry point
385
+ if (import.meta.url === `file://${process.argv[1]}`) {
386
+ const port = parseInt(process.env.PORT || '8080');
387
+ const geminiApiKey = process.env.GOOGLE_GEMINI_API_KEY;
388
+ if (!geminiApiKey) {
389
+ console.error('❌ Error: GOOGLE_GEMINI_API_KEY environment variable required');
390
+ process.exit(1);
391
+ }
392
+ const proxy = new WebSocketProxy({
393
+ port,
394
+ geminiApiKey,
395
+ geminiBaseUrl: process.env.GEMINI_BASE_URL,
396
+ pingInterval: 30000,
397
+ pingTimeout: 60000
398
+ });
399
+ proxy.start();
400
+ // Graceful shutdown
401
+ process.on('SIGINT', async () => {
402
+ console.log('\n🛑 Shutting down WebSocket proxy...');
403
+ await proxy.stop();
404
+ process.exit(0);
405
+ });
406
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Simple API key authentication for proxy
3
+ */
4
+ export class AuthManager {
5
+ validKeys;
6
+ constructor(apiKeys) {
7
+ // Load from environment or provided keys
8
+ const envKeys = process.env.PROXY_API_KEYS?.split(',').map(k => k.trim()).filter(Boolean) || [];
9
+ const allKeys = [...(apiKeys || []), ...envKeys];
10
+ this.validKeys = new Set(allKeys);
11
+ if (this.validKeys.size === 0) {
12
+ console.warn('⚠️ Warning: No API keys configured for authentication');
13
+ console.warn(' Set PROXY_API_KEYS environment variable or pass keys to constructor');
14
+ }
15
+ }
16
+ authenticate(headers) {
17
+ // If no keys configured, allow all (development mode)
18
+ if (this.validKeys.size === 0) {
19
+ return true;
20
+ }
21
+ // Check x-api-key header
22
+ const apiKey = this.extractApiKey(headers);
23
+ if (!apiKey) {
24
+ return false;
25
+ }
26
+ return this.validKeys.has(apiKey);
27
+ }
28
+ extractApiKey(headers) {
29
+ // Try x-api-key header
30
+ let key = headers['x-api-key'];
31
+ // Try authorization header (Bearer token)
32
+ if (!key) {
33
+ const auth = headers['authorization'];
34
+ if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
35
+ key = auth.substring(7);
36
+ }
37
+ }
38
+ if (Array.isArray(key)) {
39
+ key = key[0];
40
+ }
41
+ return (typeof key === 'string' && key.length > 0) ? key : null;
42
+ }
43
+ addKey(key) {
44
+ this.validKeys.add(key);
45
+ }
46
+ removeKey(key) {
47
+ this.validKeys.delete(key);
48
+ }
49
+ hasKeys() {
50
+ return this.validKeys.size > 0;
51
+ }
52
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Compression Middleware for Proxy Responses
3
+ * Provides 30-70% bandwidth reduction with Brotli/Gzip
4
+ */
5
+ import { brotliCompress, gzip, constants } from 'zlib';
6
+ import { promisify } from 'util';
7
+ import { logger } from './logger.js';
8
+ const brotliCompressAsync = promisify(brotliCompress);
9
+ const gzipAsync = promisify(gzip);
10
+ export class CompressionMiddleware {
11
+ config;
12
+ constructor(config = {}) {
13
+ this.config = {
14
+ minSize: config.minSize || 1024, // 1KB minimum
15
+ level: config.level ?? constants.BROTLI_DEFAULT_QUALITY,
16
+ preferredEncoding: config.preferredEncoding || 'br',
17
+ enableBrotli: config.enableBrotli ?? true,
18
+ enableGzip: config.enableGzip ?? true
19
+ };
20
+ }
21
+ /**
22
+ * Compress data using best available algorithm
23
+ */
24
+ async compress(data, acceptedEncodings) {
25
+ const startTime = Date.now();
26
+ const originalSize = data.length;
27
+ // Skip compression for small payloads
28
+ if (originalSize < this.config.minSize) {
29
+ return {
30
+ compressed: data,
31
+ encoding: 'identity',
32
+ originalSize,
33
+ compressedSize: originalSize,
34
+ ratio: 1.0,
35
+ duration: 0
36
+ };
37
+ }
38
+ // Determine encoding based on accept-encoding header
39
+ const encoding = this.selectEncoding(acceptedEncodings);
40
+ let compressed;
41
+ try {
42
+ switch (encoding) {
43
+ case 'br':
44
+ compressed = await this.compressBrotli(data);
45
+ break;
46
+ case 'gzip':
47
+ compressed = await this.compressGzip(data);
48
+ break;
49
+ default:
50
+ compressed = data;
51
+ }
52
+ }
53
+ catch (error) {
54
+ logger.error('Compression failed, using uncompressed', {
55
+ error: error.message
56
+ });
57
+ compressed = data;
58
+ }
59
+ const duration = Date.now() - startTime;
60
+ const compressedSize = compressed.length;
61
+ const ratio = compressedSize / originalSize;
62
+ logger.debug('Compression complete', {
63
+ encoding,
64
+ originalSize,
65
+ compressedSize,
66
+ ratio: `${(ratio * 100).toFixed(2)}%`,
67
+ savings: `${((1 - ratio) * 100).toFixed(2)}%`,
68
+ duration
69
+ });
70
+ return {
71
+ compressed,
72
+ encoding,
73
+ originalSize,
74
+ compressedSize,
75
+ ratio,
76
+ duration
77
+ };
78
+ }
79
+ /**
80
+ * Compress using Brotli (best compression ratio)
81
+ */
82
+ async compressBrotli(data) {
83
+ return brotliCompressAsync(data, {
84
+ params: {
85
+ [constants.BROTLI_PARAM_QUALITY]: this.config.level,
86
+ [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT
87
+ }
88
+ });
89
+ }
90
+ /**
91
+ * Compress using Gzip (faster, broader support)
92
+ */
93
+ async compressGzip(data) {
94
+ return gzipAsync(data, {
95
+ level: Math.min(this.config.level, 9) // Gzip max level is 9
96
+ });
97
+ }
98
+ /**
99
+ * Select best encoding based on client support
100
+ */
101
+ selectEncoding(acceptedEncodings) {
102
+ if (!acceptedEncodings) {
103
+ return this.config.preferredEncoding === 'br' && this.config.enableBrotli
104
+ ? 'br'
105
+ : this.config.enableGzip
106
+ ? 'gzip'
107
+ : 'identity';
108
+ }
109
+ const encodings = acceptedEncodings.toLowerCase().split(',').map(e => e.trim());
110
+ // Prefer Brotli if supported and enabled
111
+ if (encodings.includes('br') && this.config.enableBrotli) {
112
+ return 'br';
113
+ }
114
+ // Fall back to Gzip if supported and enabled
115
+ if (encodings.includes('gzip') && this.config.enableGzip) {
116
+ return 'gzip';
117
+ }
118
+ return 'identity';
119
+ }
120
+ /**
121
+ * Check if compression is recommended for content type
122
+ */
123
+ shouldCompress(contentType) {
124
+ if (!contentType)
125
+ return true;
126
+ const type = contentType.toLowerCase();
127
+ // Compressible types
128
+ const compressible = [
129
+ 'text/',
130
+ 'application/json',
131
+ 'application/javascript',
132
+ 'application/xml',
133
+ 'application/x-www-form-urlencoded'
134
+ ];
135
+ return compressible.some(prefix => type.includes(prefix));
136
+ }
137
+ /**
138
+ * Get compression statistics
139
+ */
140
+ getStats() {
141
+ return {
142
+ config: this.config,
143
+ capabilities: {
144
+ brotli: this.config.enableBrotli,
145
+ gzip: this.config.enableGzip
146
+ }
147
+ };
148
+ }
149
+ }