smartcontext-proxy 0.1.0 → 0.2.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 (48) 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 +34 -0
  8. package/dist/src/proxy/connect-proxy.js +167 -0
  9. package/dist/src/proxy/tls-interceptor.d.ts +23 -0
  10. package/dist/src/proxy/tls-interceptor.js +211 -0
  11. package/dist/src/proxy/tunnel.d.ts +7 -0
  12. package/dist/src/proxy/tunnel.js +33 -0
  13. package/dist/src/system/installer.d.ts +25 -0
  14. package/dist/src/system/installer.js +180 -0
  15. package/dist/src/system/linux.d.ts +11 -0
  16. package/dist/src/system/linux.js +60 -0
  17. package/dist/src/system/macos.d.ts +24 -0
  18. package/dist/src/system/macos.js +98 -0
  19. package/dist/src/system/watchdog.d.ts +7 -0
  20. package/dist/src/system/watchdog.js +115 -0
  21. package/dist/src/test/connect-proxy.test.d.ts +1 -0
  22. package/dist/src/test/connect-proxy.test.js +147 -0
  23. package/dist/src/tls/ca-manager.d.ts +9 -0
  24. package/dist/src/tls/ca-manager.js +117 -0
  25. package/dist/src/tls/trust-store.d.ts +11 -0
  26. package/dist/src/tls/trust-store.js +121 -0
  27. package/dist/src/tray/bridge.d.ts +8 -0
  28. package/dist/src/tray/bridge.js +66 -0
  29. package/dist/src/ui/ws-feed.d.ts +8 -0
  30. package/dist/src/ui/ws-feed.js +30 -0
  31. package/native/macos/SmartContextTray/Package.swift +13 -0
  32. package/native/macos/SmartContextTray/Sources/main.swift +206 -0
  33. package/package.json +6 -2
  34. package/src/context/ab-test.ts +172 -0
  35. package/src/index.ts +104 -74
  36. package/src/proxy/classifier.ts +71 -0
  37. package/src/proxy/connect-proxy.ts +187 -0
  38. package/src/proxy/tls-interceptor.ts +261 -0
  39. package/src/proxy/tunnel.ts +32 -0
  40. package/src/system/installer.ts +148 -0
  41. package/src/system/linux.ts +57 -0
  42. package/src/system/macos.ts +89 -0
  43. package/src/system/watchdog.ts +76 -0
  44. package/src/test/connect-proxy.test.ts +170 -0
  45. package/src/tls/ca-manager.ts +140 -0
  46. package/src/tls/trust-store.ts +123 -0
  47. package/src/tray/bridge.ts +61 -0
  48. package/src/ui/ws-feed.ts +32 -0
package/PLAN-v2.md ADDED
@@ -0,0 +1,390 @@
1
+ # SmartContext Proxy v2.0 — Transparent Network Interceptor
2
+
3
+ ## Goal
4
+
5
+ One-command install transparent proxy that automatically intercepts all LLM API traffic from any app on the system, optimizes context windows, and provides real-time control via system tray + web dashboard.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ Any App (CC, OC, curl, Python, browser...)
11
+
12
+ │ HTTPS → api.anthropic.com:443
13
+
14
+
15
+ [macOS System Proxy / pf / Linux iptables]
16
+
17
+
18
+ ┌──────────────────────────────────────┐
19
+ │ SmartContext Proxy (:4800) │
20
+ │ │
21
+ │ ┌─ CONNECT Handler ──────────────┐ │
22
+ │ │ Incoming HTTPS CONNECT │ │
23
+ │ │ ↓ │ │
24
+ │ │ Is hostname an LLM provider? │ │
25
+ │ │ ├─ NO → TCP tunnel (zero cost)│ │
26
+ │ │ └─ YES → TLS intercept ──┐ │ │
27
+ │ └────────────────────────────┘ │ │
28
+ │ │ │
29
+ │ ┌─ LLM Interceptor ──────────┐ │ │
30
+ │ │ TLS terminate (local CA) │ │ │
31
+ │ │ Parse request │ │ │
32
+ │ │ Optimize context │ │ │
33
+ │ │ Forward to real provider │ │ │
34
+ │ │ Stream response back │ │ │
35
+ │ │ Index exchange (async) │ │ │
36
+ │ └─────────────────────────────┘ │ │
37
+ │ │
38
+ │ ┌─ Dashboard (:4800/) ───────────┐ │
39
+ │ │ Real-time stats, live feed │ │
40
+ │ │ Savings, controls, settings │ │
41
+ │ └────────────────────────────────┘ │
42
+ │ │
43
+ │ ┌─ System Tray ──────────────────┐ │
44
+ │ │ Status icon (green/yellow/red)│ │
45
+ │ │ Click → open dashboard │ │
46
+ │ │ Quick: pause/resume/quit │ │
47
+ │ │ Tooltip: "142 req, $63 saved" │ │
48
+ │ └────────────────────────────────┘ │
49
+ └──────────────────────────────────────┘
50
+
51
+
52
+ Real LLM Provider (api.anthropic.com, api.openai.com, ...)
53
+ ```
54
+
55
+ ## Implementation Phases
56
+
57
+ ### Phase 1: HTTPS CONNECT Proxy + Selective MITM
58
+
59
+ **Goal**: Proxy intercepts HTTPS traffic, passes through non-LLM, MITM's LLM endpoints.
60
+
61
+ #### 1.1 Local CA Certificate Manager
62
+ - Generate RSA 2048 root CA cert + key on first run
63
+ - Store in `~/.smartcontext/ca/` (root-ca.crt, root-ca.key)
64
+ - Generate per-hostname certs on-the-fly (cached in `~/.smartcontext/ca/hosts/`)
65
+ - Uses `node-forge` for cert generation (pure JS, no native deps)
66
+ - **Files**: `src/tls/ca-manager.ts`
67
+
68
+ #### 1.2 CA Trust Store Installation
69
+ - macOS: `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain`
70
+ - Linux: copy to `/usr/local/share/ca-certificates/`, run `update-ca-certificates`
71
+ - Node.js: set `NODE_EXTRA_CA_CERTS` env var
72
+ - Uninstall: reverse all of the above
73
+ - **Files**: `src/tls/trust-store.ts`
74
+
75
+ #### 1.3 CONNECT Proxy Server
76
+ - HTTP proxy server handling CONNECT method
77
+ - For non-LLM hosts: blind TCP tunnel (zero overhead, no TLS termination)
78
+ - For LLM hosts: TLS intercept with generated cert
79
+ - Known LLM hosts list: `api.anthropic.com`, `api.openai.com`, `generativelanguage.googleapis.com`, `openrouter.ai`, `localhost:11434` (Ollama)
80
+ - Configurable host patterns for custom providers
81
+ - **Files**: `src/proxy/connect-proxy.ts`, `src/proxy/tunnel.ts`
82
+
83
+ #### 1.4 TLS Interceptor
84
+ - Terminate incoming TLS with per-host cert (signed by local CA)
85
+ - Parse HTTP request inside tunnel
86
+ - Apply context optimization
87
+ - Establish new TLS connection to real provider
88
+ - Stream response back through tunnel
89
+ - **Files**: `src/proxy/tls-interceptor.ts`
90
+
91
+ #### 1.5 Traffic Classifier
92
+ - Hostname → provider mapping (exact match + patterns)
93
+ - Path-based detection (`/v1/messages`, `/v1/chat/completions`, `/api/chat`)
94
+ - Configurable allowlist/blocklist
95
+ - **Files**: `src/proxy/classifier.ts`
96
+
97
+ #### Phase 1 Tests
98
+ - [ ] Non-LLM HTTPS traffic passes through untouched
99
+ - [ ] LLM traffic is intercepted and optimized
100
+ - [ ] Cert generated for api.anthropic.com on first request
101
+ - [ ] Streaming works through MITM tunnel
102
+ - [ ] Performance: <2ms overhead for non-LLM tunnel
103
+ - [ ] Ollama local traffic intercepted (HTTP, no TLS needed)
104
+
105
+ ---
106
+
107
+ ### Phase 2: System Integration (Auto-Configuration)
108
+
109
+ **Goal**: One command configures the OS to route traffic through SmartContext.
110
+
111
+ #### 2.1 macOS System Proxy Configuration
112
+ - `networksetup -setsecurewebproxy <interface> 127.0.0.1 4800`
113
+ - `networksetup -setwebproxy <interface> 127.0.0.1 4800`
114
+ - Auto-detect active network interface (Wi-Fi, Ethernet)
115
+ - PAC file for selective proxying (only LLM hosts through proxy)
116
+ - Bypass list for local traffic (localhost except Ollama port)
117
+ - **Files**: `src/system/macos.ts`
118
+
119
+ #### 2.2 Linux System Proxy Configuration
120
+ - Set `http_proxy`/`https_proxy` env vars system-wide
121
+ - GNOME: `gsettings set org.gnome.system.proxy`
122
+ - Alternatively: iptables REDIRECT rules for specific IPs
123
+ - **Files**: `src/system/linux.ts`
124
+
125
+ #### 2.3 Install / Uninstall Commands
126
+ - `smartcontext-proxy install`:
127
+ 1. Generate CA cert
128
+ 2. Install CA in trust store (requires sudo)
129
+ 3. Configure system proxy
130
+ 4. Start daemon
131
+ 5. Install system service (auto-start on boot)
132
+ - `smartcontext-proxy uninstall`:
133
+ 1. Stop daemon
134
+ 2. Remove system proxy config
135
+ 3. Remove CA from trust store
136
+ 4. Remove system service
137
+ 5. Optionally delete data (`--purge`)
138
+ - Atomic: if any step fails, rollback all previous
139
+ - **Files**: `src/system/installer.ts`
140
+
141
+ #### 2.4 PAC File Server
142
+ - Serve proxy auto-config at `http://localhost:4800/proxy.pac`
143
+ - Routes only LLM hostnames through proxy
144
+ - Everything else DIRECT
145
+ - Auto-update when provider list changes
146
+ - **Files**: `src/system/pac.ts`
147
+
148
+ #### Phase 2 Tests
149
+ - [ ] `install` configures system proxy on macOS
150
+ - [ ] `uninstall` cleanly removes everything
151
+ - [ ] PAC file correctly routes LLM traffic only
152
+ - [ ] Non-LLM browsing unaffected after install
153
+ - [ ] Works on both Wi-Fi and Ethernet
154
+
155
+ ---
156
+
157
+ ### Phase 3: System Tray Application
158
+
159
+ **Goal**: Native system tray icon with real-time status and quick controls.
160
+
161
+ #### 3.1 Tray Technology
162
+ - Use `@nicolo-ribaudo/trayicon` or custom native bridge
163
+ - macOS: NSStatusItem via Objective-C bridge (or Swift)
164
+ - Fallback: Electron-free `menubar`-style with native popup
165
+ - Alternative: spawn a tiny Swift binary for tray (compiled at install time)
166
+ - **Decision needed**: pure Node.js tray lib vs native Swift helper
167
+
168
+ #### 3.2 Tray Icon States
169
+ - 🟢 Green: running, optimizing
170
+ - 🟡 Yellow: paused (pass-through mode)
171
+ - 🔴 Red: error / proxy down
172
+ - Icon shows miniature savings counter
173
+
174
+ #### 3.3 Tray Menu
175
+ ```
176
+ SmartContext Proxy ● Running
177
+ ─────────────────────────────
178
+ Today: 142 requests, $63 saved
179
+ Savings: 68% avg
180
+ ─────────────────────────────
181
+ Open Dashboard ⌘D
182
+ ─────────────────────────────
183
+ ☑ Optimize Context
184
+ ☐ A/B Test Mode
185
+ ☐ Debug Headers
186
+ ─────────────────────────────
187
+ Pause Optimization
188
+ ─────────────────────────────
189
+ Settings...
190
+ Logs...
191
+ ─────────────────────────────
192
+ Quit SmartContext ⌘Q
193
+ ```
194
+
195
+ #### 3.4 Tray Click → Dashboard
196
+ - Left click: open `http://localhost:4800` in default browser
197
+ - Right click: show menu above
198
+ - Tooltip on hover: "SmartContext: 142 req, $63 saved, 68% avg"
199
+
200
+ #### 3.5 Notifications
201
+ - macOS native notifications for:
202
+ - "SmartContext installed successfully"
203
+ - "Optimization paused"
204
+ - "⚠️ Quality drop detected (A/B test)"
205
+ - Daily summary: "Saved $X today (Y requests)"
206
+
207
+ #### 3.6 Native macOS Tray (Swift helper)
208
+ - Tiny Swift binary (~50KB) for NSStatusItem
209
+ - Communicates with Node.js proxy via localhost HTTP API
210
+ - Polls `/_sc/status` every 2s for icon updates
211
+ - Compiled at `npm install` time via `swift build`
212
+ - **Files**: `native/macos/SmartContextTray/`, `src/tray/bridge.ts`
213
+
214
+ #### Phase 3 Tests
215
+ - [ ] Tray icon appears on macOS
216
+ - [ ] Icon color changes with state
217
+ - [ ] Menu items trigger correct API calls
218
+ - [ ] Click opens dashboard
219
+ - [ ] Notifications fire on key events
220
+
221
+ ---
222
+
223
+ ### Phase 4: Full Dashboard Upgrade
224
+
225
+ **Goal**: Production-grade dashboard matching SPEC.md screens.
226
+
227
+ #### 4.1 Dashboard Screens (from SPEC)
228
+ - **Home/Status**: savings cards, 7-day chart, provider status, performance
229
+ - **Live Feed**: WebSocket real-time stream, expandable rows with retrieval details
230
+ - **Sessions**: per-session breakdown, exchange viewer, export JSON
231
+ - **Savings Report**: monthly breakdown, by-provider, by-model, projections
232
+ - **Settings**: context tuning, provider management, logging, toggles
233
+ - **A/B Test Results**: comparison table, quality match rates, diff inspector
234
+
235
+ #### 4.2 WebSocket Live Feed
236
+ - `ws://localhost:4800/_sc/ws` — real-time request stream
237
+ - Push every request metric as it completes
238
+ - Dashboard auto-updates without polling
239
+ - **Files**: `src/ui/ws-feed.ts`
240
+
241
+ #### 4.3 Charts
242
+ - Lightweight `<canvas>` drawing (no chart library)
243
+ - 7-day savings trend (bar chart)
244
+ - Daily request volume (line chart)
245
+ - Per-provider pie chart
246
+ - **Files**: `src/ui/charts.ts`
247
+
248
+ #### 4.4 Settings UI
249
+ - Edit all context config from browser
250
+ - Toggle pause/resume per provider
251
+ - Manage LLM host patterns (add custom providers)
252
+ - Debug mode toggle
253
+ - Changes write to `~/.smartcontext/config.json`
254
+
255
+ #### Phase 4 Tests
256
+ - [ ] All 6 dashboard screens render
257
+ - [ ] WebSocket feed updates in real-time
258
+ - [ ] Charts display correct data
259
+ - [ ] Settings changes persist
260
+ - [ ] Dashboard loads in <200ms
261
+
262
+ ---
263
+
264
+ ### Phase 5: A/B Test Mode + LLM Diagnostics
265
+
266
+ **Goal**: Quality validation and self-tuning.
267
+
268
+ #### 5.1 A/B Test Mode
269
+ - Every request sent twice (optimized + original)
270
+ - Compare: semantic similarity, token delta, tool use match
271
+ - Dashboard shows quality match rates
272
+ - `--test-sample N%` for cost control
273
+
274
+ #### 5.2 LLM-Assisted Diagnostics
275
+ - On quality diff <0.85: diagnostic LLM analyzes what went wrong
276
+ - Auto-tune after 50+ requests
277
+ - `smartcontext-proxy diagnose` CLI command
278
+ - Dashboard "Diagnose" button per request
279
+
280
+ #### 5.3 Auto-Tuning
281
+ - Analyze score distributions, miss patterns, chunk sizes
282
+ - Propose config changes with reasoning
283
+ - User approves/rejects from dashboard
284
+
285
+ ---
286
+
287
+ ### Phase 6: Production Hardening
288
+
289
+ #### 6.1 Upstream Proxy Support
290
+ - Detect and chain through corporate proxies
291
+ - `HTTP_PROXY`/`HTTPS_PROXY` awareness
292
+ - Proxy authentication (Basic, NTLM)
293
+
294
+ #### 6.2 Cert Pinning Fallback
295
+ - Detect cert pinning failures (TLS handshake error)
296
+ - Auto-fallback to tunnel mode for pinned hosts
297
+ - Log which apps use pinning
298
+
299
+ #### 6.3 Performance
300
+ - Connection pooling to providers
301
+ - Cert caching (no regeneration per request)
302
+ - Non-LLM tunnel: zero-copy forwarding
303
+ - Benchmark: <5ms p95 overhead for LLM, <1ms for tunnel
304
+
305
+ #### 6.4 Security
306
+ - CA private key encrypted at rest (AES-256, machine-derived key)
307
+ - No plaintext API keys (env var references)
308
+ - Localhost-only binding
309
+ - Auto-lock if proxy crashes (remove system proxy config)
310
+
311
+ #### 6.5 Error Recovery
312
+ - Crash guard: if proxy dies, system proxy auto-removed (watchdog)
313
+ - Graceful degradation: if optimization fails, tunnel through
314
+ - Health monitor: auto-restart on failure
315
+
316
+ ---
317
+
318
+ ## Tech Stack
319
+
320
+ | Component | Technology |
321
+ |-----------|-----------|
322
+ | Proxy core | Node.js, raw `http`/`https`/`tls`/`net` modules |
323
+ | Cert generation | `node-forge` (pure JS) |
324
+ | Vector storage | LanceDB embedded |
325
+ | Embeddings | Ollama (GPU) / ONNX (CPU fallback) |
326
+ | Dashboard | Vanilla HTML/CSS/JS, WebSocket |
327
+ | System tray (macOS) | Swift native binary (~50KB) |
328
+ | System tray (Linux) | libappindicator / StatusNotifierItem |
329
+ | System config | `networksetup` (macOS), `gsettings`/env (Linux) |
330
+ | Token counting | `tiktoken` |
331
+
332
+ ## Dependencies (new)
333
+
334
+ - `node-forge` — TLS cert generation (pure JS)
335
+ - `ws` — WebSocket server for live feed
336
+
337
+ ## File Structure (v2)
338
+
339
+ ```
340
+ smartcontext-proxy/
341
+ ├── src/
342
+ │ ├── index.ts # CLI + entry point
343
+ │ ├── proxy/
344
+ │ │ ├── connect-proxy.ts # HTTP CONNECT proxy server
345
+ │ │ ├── tunnel.ts # Blind TCP tunnel (non-LLM)
346
+ │ │ ├── tls-interceptor.ts # TLS MITM for LLM traffic
347
+ │ │ ├── classifier.ts # LLM vs non-LLM traffic detection
348
+ │ │ ├── server.ts # (existing) request handling
349
+ │ │ ├── router.ts # (existing) provider routing
350
+ │ │ └── stream.ts # (existing) SSE pass-through
351
+ │ ├── tls/
352
+ │ │ ├── ca-manager.ts # Root CA generation + per-host certs
353
+ │ │ └── trust-store.ts # OS trust store install/uninstall
354
+ │ ├── system/
355
+ │ │ ├── installer.ts # install/uninstall orchestrator
356
+ │ │ ├── macos.ts # macOS proxy + keychain config
357
+ │ │ ├── linux.ts # Linux proxy config
358
+ │ │ └── pac.ts # PAC file generator/server
359
+ │ ├── tray/
360
+ │ │ └── bridge.ts # Node.js ↔ native tray communication
361
+ │ ├── providers/ # (existing)
362
+ │ ├── context/ # (existing)
363
+ │ ├── embedding/ # (existing)
364
+ │ ├── storage/ # (existing)
365
+ │ ├── metrics/ # (existing)
366
+ │ ├── ui/
367
+ │ │ ├── dashboard.ts # (upgrade) full dashboard
368
+ │ │ ├── ws-feed.ts # WebSocket live feed
369
+ │ │ └── charts.ts # Canvas chart rendering
370
+ │ ├── daemon/ # (existing)
371
+ │ ├── config/ # (existing)
372
+ │ └── test/
373
+ ├── native/
374
+ │ └── macos/
375
+ │ └── SmartContextTray/ # Swift tray app
376
+ ├── adapters/openclaw/ # (existing)
377
+ ├── package.json
378
+ └── README.md
379
+ ```
380
+
381
+ ## Execution Order
382
+
383
+ 1. **Phase 1** (HTTPS CONNECT + MITM) — foundational, everything depends on this
384
+ 2. **Phase 2** (System integration) — makes it actually transparent
385
+ 3. **Phase 3** (System tray) — user control from minute one
386
+ 4. **Phase 4** (Dashboard upgrade) — full observability
387
+ 5. **Phase 5** (A/B + diagnostics) — quality assurance
388
+ 6. **Phase 6** (Hardening) — production readiness
389
+
390
+ Phases 3+4 can run in parallel after Phase 2.
@@ -0,0 +1,32 @@
1
+ export interface ABComparison {
2
+ requestId: number;
3
+ timestamp: number;
4
+ provider: string;
5
+ model: string;
6
+ optimizedTokens: number;
7
+ originalTokens: number;
8
+ savingsPercent: number;
9
+ semanticSimilarity: number;
10
+ qualityMatch: boolean;
11
+ responseA: string;
12
+ responseB: string;
13
+ latencyA: number;
14
+ latencyB: number;
15
+ }
16
+ export declare function enableABTest(sampleRate?: number): void;
17
+ export declare function disableABTest(): void;
18
+ export declare function isABEnabled(): boolean;
19
+ export declare function shouldABTest(): boolean;
20
+ export declare function getABResults(limit?: number): ABComparison[];
21
+ export declare function getABSummary(): {
22
+ total: number;
23
+ qualityMatch: number;
24
+ minorDiff: number;
25
+ significantDiff: number;
26
+ avgSavings: number;
27
+ };
28
+ /**
29
+ * Send the original (unoptimized) request to the provider for comparison.
30
+ * This is Path B — the result is stored but NOT returned to the client.
31
+ */
32
+ export declare function sendOriginalForComparison(originalBody: Buffer, hostname: string, port: number, path: string, headers: Record<string, string>, requestId: number, provider: string, model: string, optimizedResponse: string, optimizedTokens: number, originalTokens: number, savingsPercent: number): Promise<void>;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.enableABTest = enableABTest;
7
+ exports.disableABTest = disableABTest;
8
+ exports.isABEnabled = isABEnabled;
9
+ exports.shouldABTest = shouldABTest;
10
+ exports.getABResults = getABResults;
11
+ exports.getABSummary = getABSummary;
12
+ exports.sendOriginalForComparison = sendOriginalForComparison;
13
+ const node_http_1 = __importDefault(require("node:http"));
14
+ const node_https_1 = __importDefault(require("node:https"));
15
+ let abResults = [];
16
+ let abEnabled = false;
17
+ let abSampleRate = 100; // percent
18
+ function enableABTest(sampleRate = 100) {
19
+ abEnabled = true;
20
+ abSampleRate = Math.min(100, Math.max(1, sampleRate));
21
+ }
22
+ function disableABTest() {
23
+ abEnabled = false;
24
+ }
25
+ function isABEnabled() {
26
+ return abEnabled;
27
+ }
28
+ function shouldABTest() {
29
+ if (!abEnabled)
30
+ return false;
31
+ return Math.random() * 100 < abSampleRate;
32
+ }
33
+ function getABResults(limit = 50) {
34
+ return abResults.slice(-limit);
35
+ }
36
+ function getABSummary() {
37
+ const total = abResults.length;
38
+ if (total === 0)
39
+ return { total: 0, qualityMatch: 0, minorDiff: 0, significantDiff: 0, avgSavings: 0 };
40
+ let qualityMatch = 0;
41
+ let minorDiff = 0;
42
+ let significantDiff = 0;
43
+ let totalSavings = 0;
44
+ for (const r of abResults) {
45
+ if (r.semanticSimilarity >= 0.95)
46
+ qualityMatch++;
47
+ else if (r.semanticSimilarity >= 0.85)
48
+ minorDiff++;
49
+ else
50
+ significantDiff++;
51
+ totalSavings += r.savingsPercent;
52
+ }
53
+ return {
54
+ total,
55
+ qualityMatch,
56
+ minorDiff,
57
+ significantDiff,
58
+ avgSavings: Math.round(totalSavings / total),
59
+ };
60
+ }
61
+ /**
62
+ * Send the original (unoptimized) request to the provider for comparison.
63
+ * This is Path B — the result is stored but NOT returned to the client.
64
+ */
65
+ async function sendOriginalForComparison(originalBody, hostname, port, path, headers, requestId, provider, model, optimizedResponse, optimizedTokens, originalTokens, savingsPercent) {
66
+ const startTime = Date.now();
67
+ try {
68
+ const useHTTPS = port === 443 || hostname.includes('.');
69
+ const transport = useHTTPS ? node_https_1.default : node_http_1.default;
70
+ const url = `${useHTTPS ? 'https' : 'http'}://${hostname}:${port}${path}`;
71
+ const forwardHeaders = { ...headers };
72
+ forwardHeaders['content-length'] = String(originalBody.length);
73
+ const responseB = await new Promise((resolve, reject) => {
74
+ const req = transport.request(url, { method: 'POST', headers: forwardHeaders }, (res) => {
75
+ let data = '';
76
+ res.on('data', (chunk) => (data += chunk));
77
+ res.on('end', () => resolve(data));
78
+ });
79
+ req.on('error', reject);
80
+ req.write(originalBody);
81
+ req.end();
82
+ });
83
+ const latencyB = Date.now() - startTime;
84
+ // Simple text similarity (cosine of character trigrams)
85
+ const similarity = textSimilarity(optimizedResponse, responseB);
86
+ abResults.push({
87
+ requestId,
88
+ timestamp: Date.now(),
89
+ provider,
90
+ model,
91
+ optimizedTokens,
92
+ originalTokens,
93
+ savingsPercent,
94
+ semanticSimilarity: similarity,
95
+ qualityMatch: similarity >= 0.95,
96
+ responseA: optimizedResponse.slice(0, 500),
97
+ responseB: responseB.slice(0, 500),
98
+ latencyA: 0, // filled externally
99
+ latencyB,
100
+ });
101
+ // Keep max 1000 results
102
+ if (abResults.length > 1000) {
103
+ abResults = abResults.slice(-500);
104
+ }
105
+ }
106
+ catch {
107
+ // A/B test failure is non-critical
108
+ }
109
+ }
110
+ /** Simple text similarity using character trigrams */
111
+ function textSimilarity(a, b) {
112
+ if (a === b)
113
+ return 1.0;
114
+ if (!a || !b)
115
+ return 0;
116
+ const trigramsA = getTrigrams(a.toLowerCase());
117
+ const trigramsB = getTrigrams(b.toLowerCase());
118
+ let intersection = 0;
119
+ for (const t of trigramsA) {
120
+ if (trigramsB.has(t))
121
+ intersection++;
122
+ }
123
+ const union = trigramsA.size + trigramsB.size - intersection;
124
+ return union === 0 ? 0 : intersection / union;
125
+ }
126
+ function getTrigrams(s) {
127
+ const set = new Set();
128
+ for (let i = 0; i < s.length - 2; i++) {
129
+ set.add(s.substring(i, i + 3));
130
+ }
131
+ return set;
132
+ }
133
+ //# sourceMappingURL=ab-test.js.map