smartcontext-proxy 0.1.0 → 0.2.1

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 (64) hide show
  1. package/PLAN-v2.md +390 -0
  2. package/dist/src/context/ab-test.d.ts +32 -0
  3. package/dist/src/context/ab-test.js +133 -0
  4. package/dist/src/index.js +99 -78
  5. package/dist/src/proxy/classifier.d.ts +14 -0
  6. package/dist/src/proxy/classifier.js +63 -0
  7. package/dist/src/proxy/connect-proxy.d.ts +37 -0
  8. package/dist/src/proxy/connect-proxy.js +234 -0
  9. package/dist/src/proxy/server.js +10 -1
  10. package/dist/src/proxy/tls-interceptor.d.ts +23 -0
  11. package/dist/src/proxy/tls-interceptor.js +211 -0
  12. package/dist/src/proxy/transparent-listener.d.ts +31 -0
  13. package/dist/src/proxy/transparent-listener.js +285 -0
  14. package/dist/src/proxy/tunnel.d.ts +7 -0
  15. package/dist/src/proxy/tunnel.js +33 -0
  16. package/dist/src/system/dns-redirect.d.ts +28 -0
  17. package/dist/src/system/dns-redirect.js +141 -0
  18. package/dist/src/system/installer.d.ts +25 -0
  19. package/dist/src/system/installer.js +180 -0
  20. package/dist/src/system/linux.d.ts +11 -0
  21. package/dist/src/system/linux.js +60 -0
  22. package/dist/src/system/macos.d.ts +24 -0
  23. package/dist/src/system/macos.js +98 -0
  24. package/dist/src/system/pf-redirect.d.ts +25 -0
  25. package/dist/src/system/pf-redirect.js +177 -0
  26. package/dist/src/system/watchdog.d.ts +7 -0
  27. package/dist/src/system/watchdog.js +115 -0
  28. package/dist/src/test/connect-proxy.test.d.ts +1 -0
  29. package/dist/src/test/connect-proxy.test.js +147 -0
  30. package/dist/src/test/dashboard.test.js +1 -0
  31. package/dist/src/tls/ca-manager.d.ts +9 -0
  32. package/dist/src/tls/ca-manager.js +117 -0
  33. package/dist/src/tls/trust-store.d.ts +11 -0
  34. package/dist/src/tls/trust-store.js +121 -0
  35. package/dist/src/tray/bridge.d.ts +8 -0
  36. package/dist/src/tray/bridge.js +66 -0
  37. package/dist/src/ui/dashboard.d.ts +10 -1
  38. package/dist/src/ui/dashboard.js +119 -34
  39. package/dist/src/ui/ws-feed.d.ts +8 -0
  40. package/dist/src/ui/ws-feed.js +30 -0
  41. package/native/macos/SmartContextTray/Package.swift +13 -0
  42. package/native/macos/SmartContextTray/Sources/main.swift +206 -0
  43. package/package.json +6 -2
  44. package/src/context/ab-test.ts +172 -0
  45. package/src/index.ts +104 -74
  46. package/src/proxy/classifier.ts +71 -0
  47. package/src/proxy/connect-proxy.ts +251 -0
  48. package/src/proxy/server.ts +11 -2
  49. package/src/proxy/tls-interceptor.ts +261 -0
  50. package/src/proxy/transparent-listener.ts +328 -0
  51. package/src/proxy/tunnel.ts +32 -0
  52. package/src/system/dns-redirect.ts +144 -0
  53. package/src/system/installer.ts +148 -0
  54. package/src/system/linux.ts +57 -0
  55. package/src/system/macos.ts +89 -0
  56. package/src/system/pf-redirect.ts +175 -0
  57. package/src/system/watchdog.ts +76 -0
  58. package/src/test/connect-proxy.test.ts +170 -0
  59. package/src/test/dashboard.test.ts +1 -0
  60. package/src/tls/ca-manager.ts +140 -0
  61. package/src/tls/trust-store.ts +123 -0
  62. package/src/tray/bridge.ts +61 -0
  63. package/src/ui/dashboard.ts +129 -35
  64. package/src/ui/ws-feed.ts +32 -0
@@ -0,0 +1,71 @@
1
+ /** Known LLM provider hostnames and their API patterns */
2
+
3
+ export interface ProviderMatch {
4
+ provider: string;
5
+ hostname: string;
6
+ isLLM: true;
7
+ }
8
+
9
+ const LLM_HOSTS: Record<string, string> = {
10
+ 'api.anthropic.com': 'anthropic',
11
+ 'api.openai.com': 'openai',
12
+ 'generativelanguage.googleapis.com': 'google',
13
+ 'openrouter.ai': 'openrouter',
14
+ 'api.together.xyz': 'together',
15
+ 'api.fireworks.ai': 'fireworks',
16
+ 'api.mistral.ai': 'mistral',
17
+ 'api.cohere.com': 'cohere',
18
+ 'api.groq.com': 'groq',
19
+ 'api.deepseek.com': 'deepseek',
20
+ };
21
+
22
+ /** Ollama ports to intercept (HTTP, no TLS) */
23
+ const OLLAMA_PORTS = new Set([11434]);
24
+
25
+ /** Custom hosts added via config */
26
+ let customHosts: Record<string, string> = {};
27
+
28
+ export function addCustomHost(hostname: string, provider: string): void {
29
+ customHosts[hostname] = provider;
30
+ }
31
+
32
+ export function removeCustomHost(hostname: string): void {
33
+ delete customHosts[hostname];
34
+ }
35
+
36
+ /** Check if a hostname:port is an LLM provider that should be intercepted */
37
+ export function classifyHost(hostname: string, port: number): ProviderMatch | null {
38
+ // Check known LLM hosts
39
+ const provider = LLM_HOSTS[hostname] || customHosts[hostname];
40
+ if (provider) {
41
+ return { provider, hostname, isLLM: true };
42
+ }
43
+
44
+ // Check Ollama local
45
+ if ((hostname === 'localhost' || hostname === '127.0.0.1') && OLLAMA_PORTS.has(port)) {
46
+ return { provider: 'ollama', hostname, isLLM: true };
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ /** Get all known LLM hostnames (for PAC file generation) */
53
+ export function getLLMHostnames(): string[] {
54
+ return [
55
+ ...Object.keys(LLM_HOSTS),
56
+ ...Object.keys(customHosts),
57
+ ];
58
+ }
59
+
60
+ /** Check if a request path looks like an LLM API call */
61
+ export function isLLMPath(path: string): boolean {
62
+ const llmPaths = [
63
+ '/v1/messages', // Anthropic
64
+ '/v1/chat/completions', // OpenAI
65
+ '/v1/completions', // OpenAI legacy
66
+ '/api/chat', // Ollama
67
+ '/api/generate', // Ollama
68
+ '/v1beta/models', // Google
69
+ ];
70
+ return llmPaths.some((p) => path.startsWith(p));
71
+ }
@@ -0,0 +1,251 @@
1
+ import http from 'node:http';
2
+ import type { Socket } from 'node:net';
3
+ import { classifyHost } from './classifier.js';
4
+ import { createTunnel } from './tunnel.js';
5
+ import { interceptTLS, type InterceptorOptions } from './tls-interceptor.js';
6
+ import type { SmartContextConfig } from '../config/schema.js';
7
+ import type { ProviderAdapter } from '../providers/types.js';
8
+ import { ContextOptimizer } from '../context/optimizer.js';
9
+ import { MetricsCollector } from '../metrics/collector.js';
10
+ import { renderDashboard, type DashboardState } from '../ui/dashboard.js';
11
+ import { isABEnabled, enableABTest, disableABTest, getABSummary, getABResults } from '../context/ab-test.js';
12
+ import { isCAInstalled } from '../tls/trust-store.js';
13
+ import { cacheRealIPs, getRealIP, isDNSRedirectActive, enableDNSRedirect, disableDNSRedirect } from '../system/dns-redirect.js';
14
+
15
+ /**
16
+ * HTTP CONNECT proxy that transparently intercepts LLM traffic.
17
+ *
18
+ * - Non-LLM HTTPS: blind TCP tunnel (zero overhead)
19
+ * - LLM HTTPS: TLS intercept → optimize → forward
20
+ * - HTTP requests: direct handling (dashboard, API, Ollama)
21
+ */
22
+ export class ConnectProxy {
23
+ private server: http.Server;
24
+ private metrics = new MetricsCollector();
25
+ private optimizer: ContextOptimizer | null = null;
26
+ private adapters = new Map<string, ProviderAdapter>();
27
+ private paused = false;
28
+ private debugHeaders = false;
29
+ private systemProxyActive = false;
30
+ private requestCounter = { value: 0 };
31
+ private config: SmartContextConfig;
32
+
33
+ constructor(
34
+ config: SmartContextConfig,
35
+ optimizer?: ContextOptimizer | null,
36
+ adapters?: Map<string, ProviderAdapter>,
37
+ ) {
38
+ this.config = config;
39
+ this.optimizer = optimizer || null;
40
+ if (adapters) this.adapters = adapters;
41
+
42
+ this.server = http.createServer((req, res) => this.handleHTTP(req, res));
43
+ this.server.on('connect', (req, clientSocket: Socket, head) => this.handleConnect(req, clientSocket, head));
44
+ }
45
+
46
+ async start(): Promise<void> {
47
+ const { port, host } = this.config.proxy;
48
+ return new Promise((resolve) => {
49
+ this.server.listen(port, host, () => resolve());
50
+ });
51
+ }
52
+
53
+ async stop(): Promise<void> {
54
+ return new Promise((resolve) => {
55
+ this.server.close(() => resolve());
56
+ });
57
+ }
58
+
59
+ getMetrics(): MetricsCollector { return this.metrics; }
60
+ isPaused(): boolean { return this.paused; }
61
+ setPaused(v: boolean): void { this.paused = v; }
62
+
63
+ /** Handle HTTP CONNECT requests (HTTPS tunnel establishment) */
64
+ private handleConnect(req: http.IncomingMessage, clientSocket: Socket, head: Buffer): void {
65
+ const [hostname, portStr] = (req.url || '').split(':');
66
+ const port = parseInt(portStr || '443', 10);
67
+
68
+ const match = classifyHost(hostname, port);
69
+
70
+ if (match) {
71
+ // LLM provider → intercept TLS
72
+ this.log('info', `INTERCEPT ${hostname}:${port} (${match.provider})`);
73
+ interceptTLS(clientSocket, hostname, port, match, this.interceptorOptions());
74
+ } else {
75
+ // Non-LLM → blind tunnel
76
+ createTunnel(clientSocket, hostname, port);
77
+ }
78
+ }
79
+
80
+ /** Handle plain HTTP requests (dashboard, API, Ollama interception) */
81
+ private async handleHTTP(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
82
+ const path = req.url || '/';
83
+ const method = req.method || 'GET';
84
+
85
+ // Dashboard
86
+ if (path === '/' && method === 'GET') {
87
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
88
+ res.end(renderDashboard(this.metrics, this.getDashboardState()));
89
+ return;
90
+ }
91
+
92
+ // Health
93
+ if (path === '/health') {
94
+ res.writeHead(200, { 'Content-Type': 'application/json' });
95
+ res.end(JSON.stringify({
96
+ ok: true,
97
+ requests: this.requestCounter.value,
98
+ paused: this.paused,
99
+ mode: this.optimizer ? 'optimizing' : 'transparent',
100
+ type: 'connect-proxy',
101
+ }));
102
+ return;
103
+ }
104
+
105
+ // PAC file
106
+ if (path === '/proxy.pac') {
107
+ res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' });
108
+ res.end(this.generatePAC());
109
+ return;
110
+ }
111
+
112
+ // API endpoints
113
+ if (path.startsWith('/_sc/')) {
114
+ this.handleAPI(path, method, req, res);
115
+ return;
116
+ }
117
+
118
+ // Everything else: 404
119
+ res.writeHead(404, { 'Content-Type': 'application/json' });
120
+ res.end(JSON.stringify({ error: 'Not found' }));
121
+ }
122
+
123
+ private async handleAPI(path: string, method: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
124
+ res.setHeader('Content-Type', 'application/json');
125
+
126
+ switch (path) {
127
+ case '/_sc/status':
128
+ res.end(JSON.stringify({
129
+ state: this.paused ? 'paused' : 'running',
130
+ uptime: this.metrics.getUptime(),
131
+ requests: this.requestCounter.value,
132
+ mode: this.optimizer ? 'optimizing' : 'transparent',
133
+ }));
134
+ break;
135
+ case '/_sc/stats':
136
+ res.end(JSON.stringify(this.metrics.getStats()));
137
+ break;
138
+ case '/_sc/feed':
139
+ res.end(JSON.stringify(this.metrics.getRecent(50)));
140
+ break;
141
+ case '/_sc/pause':
142
+ this.paused = true;
143
+ res.end(JSON.stringify({ ok: true, state: 'paused' }));
144
+ break;
145
+ case '/_sc/resume':
146
+ this.paused = false;
147
+ res.end(JSON.stringify({ ok: true, state: 'running' }));
148
+ break;
149
+ case '/_sc/ab-test/enable':
150
+ enableABTest();
151
+ res.end(JSON.stringify({ ok: true, abTest: true }));
152
+ break;
153
+ case '/_sc/ab-test/disable':
154
+ disableABTest();
155
+ res.end(JSON.stringify({ ok: true, abTest: false }));
156
+ break;
157
+ case '/_sc/ab-test/results':
158
+ res.end(JSON.stringify(getABResults()));
159
+ break;
160
+ case '/_sc/ab-test/summary':
161
+ res.end(JSON.stringify(getABSummary()));
162
+ break;
163
+ case '/_sc/debug-headers/enable':
164
+ this.debugHeaders = true;
165
+ this.config.logging.debug_headers = true;
166
+ res.end(JSON.stringify({ ok: true, debugHeaders: true }));
167
+ break;
168
+ case '/_sc/debug-headers/disable':
169
+ this.debugHeaders = false;
170
+ this.config.logging.debug_headers = false;
171
+ res.end(JSON.stringify({ ok: true, debugHeaders: false }));
172
+ break;
173
+ case '/_sc/system-proxy/enable':
174
+ try {
175
+ // Cache real IPs before overriding DNS
176
+ await cacheRealIPs();
177
+ const dnsResult = enableDNSRedirect();
178
+ this.systemProxyActive = dnsResult.success;
179
+ res.end(JSON.stringify({ ok: dnsResult.success, message: dnsResult.message, method: 'dns-redirect' }));
180
+ } catch (err) {
181
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
182
+ }
183
+ break;
184
+ case '/_sc/system-proxy/disable':
185
+ try {
186
+ const disableResult = disableDNSRedirect();
187
+ this.systemProxyActive = false;
188
+ res.end(JSON.stringify({ ok: disableResult.success, message: disableResult.message }));
189
+ } catch (err) {
190
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
191
+ }
192
+ break;
193
+ default:
194
+ res.writeHead(404);
195
+ res.end(JSON.stringify({ error: `Unknown: ${path}` }));
196
+ }
197
+ }
198
+
199
+ private getDashboardState(): DashboardState {
200
+ let caInstalled = false;
201
+ try { caInstalled = isCAInstalled(); } catch {}
202
+
203
+ return {
204
+ paused: this.paused,
205
+ mode: this.optimizer ? 'optimizing' : 'transparent',
206
+ proxyType: 'connect',
207
+ abTestEnabled: isABEnabled(),
208
+ debugHeaders: this.debugHeaders,
209
+ caInstalled,
210
+ systemProxyActive: this.systemProxyActive,
211
+ };
212
+ }
213
+
214
+ private generatePAC(): string {
215
+ const { getLLMHostnames } = require('./classifier.js');
216
+ const hosts = getLLMHostnames();
217
+ const { port, host } = this.config.proxy;
218
+
219
+ const conditions = hosts
220
+ .map((h: string) => ` if (dnsDomainIs(host, "${h}")) return proxy;`)
221
+ .join('\n');
222
+
223
+ return `function FindProxyForURL(url, host) {
224
+ var proxy = "PROXY ${host}:${port}";
225
+ ${conditions}
226
+ // Ollama local
227
+ if (host === "localhost" && url.indexOf(":11434") !== -1) return proxy;
228
+ return "DIRECT";
229
+ }`;
230
+ }
231
+
232
+ private interceptorOptions(): import('./tls-interceptor.js').InterceptorOptions {
233
+ return {
234
+ config: this.config,
235
+ optimizer: this.optimizer,
236
+ metrics: this.metrics,
237
+ adapters: this.adapters,
238
+ paused: this.paused,
239
+ requestCounter: this.requestCounter,
240
+ log: this.log.bind(this),
241
+ };
242
+ }
243
+
244
+ private log(level: string, message: string): void {
245
+ const timestamp = new Date().toISOString().slice(11, 23);
246
+ const prefix = level === 'error' ? '✗' : '→';
247
+ if (level === 'error' || this.config.logging.level !== 'error') {
248
+ console.log(`[${timestamp}] ${prefix} ${message}`);
249
+ }
250
+ }
251
+ }
@@ -11,7 +11,7 @@ import type { EmbeddingAdapter } from '../embedding/types.js';
11
11
  import type { StorageAdapter } from '../storage/types.js';
12
12
  import { estimateTokens } from '../context/chunker.js';
13
13
  import { getTextContent } from '../context/canonical.js';
14
- import { renderDashboard } from '../ui/dashboard.js';
14
+ import { renderDashboard, type DashboardState } from '../ui/dashboard.js';
15
15
 
16
16
  export class ProxyServer {
17
17
  private server: http.Server;
@@ -72,7 +72,16 @@ export class ProxyServer {
72
72
  // Dashboard (root path)
73
73
  if (path === '/' && method === 'GET') {
74
74
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
75
- res.end(renderDashboard(this.metrics, this.paused));
75
+ const state: DashboardState = {
76
+ paused: this.paused,
77
+ mode: this.optimizer ? 'optimizing' : 'transparent',
78
+ proxyType: 'legacy',
79
+ abTestEnabled: false,
80
+ debugHeaders: this.config.logging.debug_headers,
81
+ caInstalled: false,
82
+ systemProxyActive: false,
83
+ };
84
+ res.end(renderDashboard(this.metrics, state));
76
85
  return;
77
86
  }
78
87
 
@@ -0,0 +1,261 @@
1
+ import tls from 'node:tls';
2
+ import http from 'node:http';
3
+ import https from 'node:https';
4
+ import net from 'node:net';
5
+ import { URL } from 'node:url';
6
+ import type { Socket } from 'node:net';
7
+ import { getCertForHost } from '../tls/ca-manager.js';
8
+ import { classifyHost, type ProviderMatch } from './classifier.js';
9
+ import { streamResponse } from './stream.js';
10
+ import type { ProviderAdapter } from '../providers/types.js';
11
+ import type { SmartContextConfig } from '../config/schema.js';
12
+ import { ContextOptimizer } from '../context/optimizer.js';
13
+ import { MetricsCollector, type RequestMetric } from '../metrics/collector.js';
14
+ import { estimateTokens } from '../context/chunker.js';
15
+ import { getTextContent } from '../context/canonical.js';
16
+
17
+ export interface InterceptorOptions {
18
+ config: SmartContextConfig;
19
+ optimizer: ContextOptimizer | null;
20
+ metrics: MetricsCollector;
21
+ adapters: Map<string, ProviderAdapter>;
22
+ paused: boolean;
23
+ requestCounter: { value: number };
24
+ log: (level: string, message: string) => void;
25
+ }
26
+
27
+ /**
28
+ * Intercept TLS connection to an LLM provider.
29
+ * Terminates TLS with a generated cert, parses the HTTP request inside,
30
+ * optionally optimizes context, then forwards to the real provider.
31
+ */
32
+ export function interceptTLS(
33
+ clientSocket: Socket,
34
+ hostname: string,
35
+ port: number,
36
+ match: ProviderMatch,
37
+ options: InterceptorOptions,
38
+ ): void {
39
+ const { cert, key } = getCertForHost(hostname);
40
+
41
+ const tlsSocket = new tls.TLSSocket(clientSocket, {
42
+ isServer: true,
43
+ cert,
44
+ key,
45
+ });
46
+
47
+ // Tell client the tunnel is established
48
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
49
+
50
+ // Read HTTP request from decrypted TLS stream
51
+ let requestData = Buffer.alloc(0);
52
+ let headersParsed = false;
53
+ let contentLength = 0;
54
+ let headerEnd = -1;
55
+
56
+ tlsSocket.on('data', (chunk: Buffer) => {
57
+ requestData = Buffer.concat([requestData, chunk]);
58
+
59
+ if (!headersParsed) {
60
+ headerEnd = requestData.indexOf('\r\n\r\n');
61
+ if (headerEnd === -1) return; // Wait for more data
62
+
63
+ headersParsed = true;
64
+ const headersStr = requestData.subarray(0, headerEnd).toString();
65
+ const clMatch = headersStr.match(/content-length:\s*(\d+)/i);
66
+ contentLength = clMatch ? parseInt(clMatch[1], 10) : 0;
67
+ }
68
+
69
+ const bodyStart = headerEnd + 4;
70
+ const bodyReceived = requestData.length - bodyStart;
71
+
72
+ if (bodyReceived >= contentLength) {
73
+ tlsSocket.pause();
74
+ handleInterceptedRequest(
75
+ requestData, hostname, port, match, options, tlsSocket,
76
+ ).catch((err) => {
77
+ options.log('error', `Intercept error: ${err}`);
78
+ try {
79
+ tlsSocket.write('HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n');
80
+ tlsSocket.end();
81
+ } catch {}
82
+ });
83
+ }
84
+ });
85
+
86
+ tlsSocket.on('error', (err) => {
87
+ options.log('error', `TLS socket error for ${hostname}: ${err.message}`);
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Handle an intercepted HTTP request: parse, optimize, forward, stream back.
93
+ */
94
+ async function handleInterceptedRequest(
95
+ rawRequest: Buffer,
96
+ hostname: string,
97
+ port: number,
98
+ match: ProviderMatch,
99
+ options: InterceptorOptions,
100
+ clientTLS: tls.TLSSocket,
101
+ ): Promise<void> {
102
+ const startTime = Date.now();
103
+ options.requestCounter.value++;
104
+ const reqId = options.requestCounter.value;
105
+
106
+ // Parse raw HTTP request
107
+ const headerEnd = rawRequest.indexOf('\r\n\r\n');
108
+ const headersStr = rawRequest.subarray(0, headerEnd).toString();
109
+ const body = rawRequest.subarray(headerEnd + 4);
110
+
111
+ const [requestLine, ...headerLines] = headersStr.split('\r\n');
112
+ const [method, path] = requestLine.split(' ');
113
+
114
+ const headers: Record<string, string> = {};
115
+ for (const line of headerLines) {
116
+ const colonIdx = line.indexOf(':');
117
+ if (colonIdx > 0) {
118
+ headers[line.substring(0, colonIdx).toLowerCase().trim()] = line.substring(colonIdx + 1).trim();
119
+ }
120
+ }
121
+
122
+ // Find adapter for this provider
123
+ const adapter = options.adapters.get(match.provider);
124
+ let forwardBody: Buffer;
125
+ let originalTokens = 0;
126
+ let optimizedTokens = 0;
127
+ let savingsPercent = 0;
128
+ let chunksRetrieved = 0;
129
+ let topScore = 0;
130
+ let passThrough = true;
131
+ let reason: string | undefined;
132
+ let model = 'unknown';
133
+
134
+ if (adapter && body.length > 0) {
135
+ try {
136
+ const parsed = JSON.parse(body.toString());
137
+ model = parsed.model || 'unknown';
138
+ const canonical = adapter.parseRequest(parsed, headers);
139
+
140
+ originalTokens = estimateTokens(canonical.systemPrompt || '') +
141
+ canonical.messages.reduce((sum, m) => sum + estimateTokens(getTextContent(m)), 0);
142
+
143
+ // Optimize if available and not paused
144
+ if (options.optimizer && !options.paused) {
145
+ try {
146
+ const result = await options.optimizer.optimize(canonical);
147
+ passThrough = result.passThrough;
148
+ reason = result.reason;
149
+
150
+ if (!result.passThrough) {
151
+ canonical.messages = result.optimizedMessages;
152
+ if (result.systemPrompt !== undefined) canonical.systemPrompt = result.systemPrompt;
153
+ optimizedTokens = result.packed.optimizedTokens;
154
+ savingsPercent = result.packed.savingsPercent;
155
+ }
156
+
157
+ if (result.retrieval) {
158
+ chunksRetrieved = result.retrieval.chunks.length;
159
+ topScore = result.retrieval.topScore;
160
+ }
161
+ } catch (err) {
162
+ passThrough = true;
163
+ reason = `optimization error: ${err}`;
164
+ }
165
+ }
166
+
167
+ if (!passThrough) {
168
+ forwardBody = Buffer.from(JSON.stringify(adapter.serializeRequest(canonical)));
169
+ } else {
170
+ forwardBody = body;
171
+ optimizedTokens = originalTokens;
172
+ }
173
+ } catch {
174
+ forwardBody = body;
175
+ optimizedTokens = originalTokens;
176
+ }
177
+ } else {
178
+ forwardBody = body;
179
+ }
180
+
181
+ const latencyOverhead = Date.now() - startTime;
182
+ const savingsStr = passThrough ? 'pass' : `-${savingsPercent}%`;
183
+ options.log('info',
184
+ `#${reqId} ${match.provider}/${model} ${originalTokens}→${optimizedTokens} ${savingsStr} ${latencyOverhead}ms [intercepted]`);
185
+
186
+ // Forward to real provider
187
+ const useHTTPS = port === 443 || hostname.includes('.');
188
+ const transport = useHTTPS ? https : http;
189
+ const forwardUrl = `${useHTTPS ? 'https' : 'http'}://${hostname}:${port}${path}`;
190
+
191
+ const forwardHeaders = { ...headers };
192
+ forwardHeaders['host'] = hostname;
193
+ forwardHeaders['content-length'] = String(forwardBody.length);
194
+
195
+ const providerRes = await new Promise<http.IncomingMessage>((resolve, reject) => {
196
+ const proxyReq = transport.request(forwardUrl, {
197
+ method,
198
+ headers: forwardHeaders,
199
+ }, resolve);
200
+ proxyReq.on('error', reject);
201
+ proxyReq.write(forwardBody);
202
+ proxyReq.end();
203
+ });
204
+
205
+ // Build response headers
206
+ const resHeaders: string[] = [
207
+ `HTTP/1.1 ${providerRes.statusCode} ${providerRes.statusMessage}`,
208
+ ];
209
+ for (const [key, val] of Object.entries(providerRes.headers)) {
210
+ if (val) {
211
+ const values = Array.isArray(val) ? val : [val];
212
+ for (const v of values) {
213
+ resHeaders.push(`${key}: ${v}`);
214
+ }
215
+ }
216
+ }
217
+ resHeaders.push('', '');
218
+
219
+ clientTLS.write(resHeaders.join('\r\n'));
220
+
221
+ // Stream response body
222
+ await new Promise<void>((resolve) => {
223
+ providerRes.on('data', (chunk) => {
224
+ clientTLS.write(chunk);
225
+ });
226
+ providerRes.on('end', () => {
227
+ clientTLS.end();
228
+ resolve();
229
+ });
230
+ providerRes.on('error', () => {
231
+ clientTLS.end();
232
+ resolve();
233
+ });
234
+ });
235
+
236
+ // Record metrics
237
+ options.metrics.record({
238
+ id: reqId,
239
+ timestamp: Date.now(),
240
+ provider: match.provider,
241
+ model,
242
+ streaming: headers['accept']?.includes('text/event-stream') || false,
243
+ originalTokens,
244
+ optimizedTokens,
245
+ savingsPercent,
246
+ latencyOverheadMs: latencyOverhead,
247
+ chunksRetrieved,
248
+ topScore,
249
+ passThrough,
250
+ reason,
251
+ });
252
+
253
+ // Async post-indexing
254
+ if (options.optimizer && !passThrough && adapter) {
255
+ try {
256
+ const parsed = JSON.parse(body.toString());
257
+ const canonical = adapter.parseRequest(parsed, headers);
258
+ options.optimizer.indexExchange(canonical.messages, `intercepted-${reqId}`).catch(() => {});
259
+ } catch {}
260
+ }
261
+ }