smartcontext-proxy 0.2.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.
- package/dist/src/proxy/connect-proxy.d.ts +3 -0
- package/dist/src/proxy/connect-proxy.js +69 -2
- package/dist/src/proxy/server.js +10 -1
- package/dist/src/proxy/transparent-listener.d.ts +31 -0
- package/dist/src/proxy/transparent-listener.js +285 -0
- package/dist/src/system/dns-redirect.d.ts +28 -0
- package/dist/src/system/dns-redirect.js +141 -0
- package/dist/src/system/pf-redirect.d.ts +25 -0
- package/dist/src/system/pf-redirect.js +177 -0
- package/dist/src/test/dashboard.test.js +1 -0
- package/dist/src/ui/dashboard.d.ts +10 -1
- package/dist/src/ui/dashboard.js +119 -34
- package/package.json +1 -1
- package/src/proxy/connect-proxy.ts +67 -3
- package/src/proxy/server.ts +11 -2
- package/src/proxy/transparent-listener.ts +328 -0
- package/src/system/dns-redirect.ts +144 -0
- package/src/system/pf-redirect.ts +175 -0
- package/src/test/dashboard.test.ts +1 -0
- package/src/ui/dashboard.ts +129 -35
|
@@ -0,0 +1,177 @@
|
|
|
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.enablePFRedirect = enablePFRedirect;
|
|
7
|
+
exports.disablePFRedirect = disablePFRedirect;
|
|
8
|
+
exports.isPFRedirectActive = isPFRedirectActive;
|
|
9
|
+
exports.refreshPFRules = refreshPFRules;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_dns_1 = __importDefault(require("node:dns"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const PF_ANCHOR = 'com.smartcontext';
|
|
15
|
+
const PF_CONF_PATH = node_path_1.default.join(process.env['HOME'] || '.', '.smartcontext', 'pf-smartcontext.conf');
|
|
16
|
+
/** Known LLM provider hostnames to intercept */
|
|
17
|
+
const LLM_HOSTS = [
|
|
18
|
+
'api.anthropic.com',
|
|
19
|
+
'api.openai.com',
|
|
20
|
+
'generativelanguage.googleapis.com',
|
|
21
|
+
'openrouter.ai',
|
|
22
|
+
'api.together.xyz',
|
|
23
|
+
'api.fireworks.ai',
|
|
24
|
+
'api.mistral.ai',
|
|
25
|
+
'api.cohere.com',
|
|
26
|
+
'api.groq.com',
|
|
27
|
+
'api.deepseek.com',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Resolve hostnames to IPs for pf rules.
|
|
31
|
+
* pf works at IP level, not DNS.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveHosts() {
|
|
34
|
+
const results = new Map();
|
|
35
|
+
for (const host of LLM_HOSTS) {
|
|
36
|
+
try {
|
|
37
|
+
const addrs = await new Promise((resolve, reject) => {
|
|
38
|
+
node_dns_1.default.resolve4(host, (err, addresses) => {
|
|
39
|
+
if (err)
|
|
40
|
+
reject(err);
|
|
41
|
+
else
|
|
42
|
+
resolve(addresses);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
results.set(host, addrs);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Host might not resolve — skip
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generate pf redirect rules.
|
|
55
|
+
* Redirects outgoing HTTPS (port 443) traffic to LLM provider IPs
|
|
56
|
+
* to our local proxy port.
|
|
57
|
+
*/
|
|
58
|
+
function generatePFConf(hostIPs, proxyPort) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`# SmartContext Proxy — auto-generated pf rules`,
|
|
61
|
+
`# Redirects LLM API traffic to local proxy`,
|
|
62
|
+
``,
|
|
63
|
+
];
|
|
64
|
+
// Collect all IPs into a pf table
|
|
65
|
+
const allIPs = [];
|
|
66
|
+
for (const [host, ips] of hostIPs) {
|
|
67
|
+
lines.push(`# ${host}: ${ips.join(', ')}`);
|
|
68
|
+
allIPs.push(...ips);
|
|
69
|
+
}
|
|
70
|
+
if (allIPs.length === 0) {
|
|
71
|
+
lines.push('# No IPs resolved — no rules');
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(`table <llm_providers> { ${allIPs.join(', ')} }`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
// Redirect outgoing HTTPS to LLM providers → local proxy
|
|
78
|
+
// rdr-to changes destination to localhost:proxyPort
|
|
79
|
+
lines.push(`rdr pass on lo0 proto tcp from any to <llm_providers> port 443 -> 127.0.0.1 port ${proxyPort}`);
|
|
80
|
+
lines.push('');
|
|
81
|
+
// Route the traffic through loopback so rdr applies
|
|
82
|
+
lines.push(`pass out on en0 route-to (lo0 127.0.0.1) proto tcp from any to <llm_providers> port 443`);
|
|
83
|
+
lines.push(`pass out on en1 route-to (lo0 127.0.0.1) proto tcp from any to <llm_providers> port 443`);
|
|
84
|
+
lines.push('');
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Install pf redirect rules.
|
|
89
|
+
* Requires sudo.
|
|
90
|
+
*/
|
|
91
|
+
async function enablePFRedirect(proxyPort) {
|
|
92
|
+
try {
|
|
93
|
+
const hostIPs = await resolveHosts();
|
|
94
|
+
let totalIPs = 0;
|
|
95
|
+
for (const ips of hostIPs.values())
|
|
96
|
+
totalIPs += ips.length;
|
|
97
|
+
if (totalIPs === 0) {
|
|
98
|
+
return { success: false, message: 'No LLM provider IPs resolved', ips: 0 };
|
|
99
|
+
}
|
|
100
|
+
const conf = generatePFConf(hostIPs, proxyPort);
|
|
101
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(PF_CONF_PATH), { recursive: true });
|
|
102
|
+
node_fs_1.default.writeFileSync(PF_CONF_PATH, conf);
|
|
103
|
+
// Load anchor into pf
|
|
104
|
+
try {
|
|
105
|
+
(0, node_child_process_1.execFileSync)('sudo', ['pfctl', '-a', PF_ANCHOR, '-f', PF_CONF_PATH], { stdio: 'pipe' });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return { success: false, message: `pfctl load failed: ${err}`, ips: totalIPs };
|
|
109
|
+
}
|
|
110
|
+
// Enable pf if not already enabled
|
|
111
|
+
try {
|
|
112
|
+
(0, node_child_process_1.execFileSync)('sudo', ['pfctl', '-e'], { stdio: 'pipe' });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Already enabled — ok
|
|
116
|
+
}
|
|
117
|
+
return { success: true, message: `pf redirect active: ${totalIPs} IPs from ${hostIPs.size} hosts → :${proxyPort}`, ips: totalIPs };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return { success: false, message: `Failed: ${err}`, ips: 0 };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Remove pf redirect rules.
|
|
125
|
+
*/
|
|
126
|
+
function disablePFRedirect() {
|
|
127
|
+
try {
|
|
128
|
+
// Flush anchor rules
|
|
129
|
+
try {
|
|
130
|
+
(0, node_child_process_1.execFileSync)('sudo', ['pfctl', '-a', PF_ANCHOR, '-F', 'all'], { stdio: 'pipe' });
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
// Remove conf file
|
|
134
|
+
try {
|
|
135
|
+
node_fs_1.default.unlinkSync(PF_CONF_PATH);
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
return { success: true, message: 'pf redirect removed' };
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
return { success: false, message: `Failed: ${err}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if pf redirect is active.
|
|
146
|
+
*/
|
|
147
|
+
function isPFRedirectActive() {
|
|
148
|
+
try {
|
|
149
|
+
const result = (0, node_child_process_1.execFileSync)('sudo', ['pfctl', '-a', PF_ANCHOR, '-sr'], {
|
|
150
|
+
encoding: 'utf-8',
|
|
151
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
152
|
+
});
|
|
153
|
+
return result.includes('rdr') || result.includes('route-to');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Refresh IP addresses (IPs can change due to DNS).
|
|
161
|
+
* Call periodically to keep rules current.
|
|
162
|
+
*/
|
|
163
|
+
async function refreshPFRules(proxyPort) {
|
|
164
|
+
const hostIPs = await resolveHosts();
|
|
165
|
+
let totalIPs = 0;
|
|
166
|
+
for (const ips of hostIPs.values())
|
|
167
|
+
totalIPs += ips.length;
|
|
168
|
+
if (totalIPs === 0)
|
|
169
|
+
return;
|
|
170
|
+
const conf = generatePFConf(hostIPs, proxyPort);
|
|
171
|
+
node_fs_1.default.writeFileSync(PF_CONF_PATH, conf);
|
|
172
|
+
try {
|
|
173
|
+
(0, node_child_process_1.execFileSync)('sudo', ['pfctl', '-a', PF_ANCHOR, '-f', PF_CONF_PATH], { stdio: 'pipe' });
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=pf-redirect.js.map
|
|
@@ -40,6 +40,7 @@ function httpRequest(url, options, body) {
|
|
|
40
40
|
node_assert_1.default.ok(res.headers['content-type']?.includes('text/html'));
|
|
41
41
|
node_assert_1.default.ok(res.body.includes('SmartContext Proxy'));
|
|
42
42
|
node_assert_1.default.ok(res.body.includes('Total Requests'));
|
|
43
|
+
node_assert_1.default.ok(res.body.includes('Settings'));
|
|
43
44
|
});
|
|
44
45
|
(0, node_test_1.it)('returns status via /_sc/status', async () => {
|
|
45
46
|
const res = await httpRequest(`http://127.0.0.1:${PORT}/_sc/status`, { method: 'GET' });
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { MetricsCollector } from '../metrics/collector.js';
|
|
2
|
-
export
|
|
2
|
+
export interface DashboardState {
|
|
3
|
+
paused: boolean;
|
|
4
|
+
mode: 'transparent' | 'optimizing';
|
|
5
|
+
proxyType: 'connect' | 'legacy';
|
|
6
|
+
abTestEnabled: boolean;
|
|
7
|
+
debugHeaders: boolean;
|
|
8
|
+
caInstalled: boolean;
|
|
9
|
+
systemProxyActive: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function renderDashboard(metrics: MetricsCollector, state: DashboardState): string;
|
package/dist/src/ui/dashboard.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderDashboard = renderDashboard;
|
|
4
|
-
function renderDashboard(metrics,
|
|
4
|
+
function renderDashboard(metrics, state) {
|
|
5
5
|
const stats = metrics.getStats();
|
|
6
6
|
const recent = metrics.getRecent(20);
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
7
|
+
const uptimeStr = formatDuration(metrics.getUptime());
|
|
8
|
+
const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
|
|
9
|
+
const stateBadge = state.paused
|
|
10
10
|
? '<span class="badge paused">PAUSED</span>'
|
|
11
11
|
: '<span class="badge running">RUNNING</span>';
|
|
12
|
-
const
|
|
12
|
+
const modeBadge = state.mode === 'optimizing'
|
|
13
|
+
? '<span class="badge opt">OPTIMIZING</span>'
|
|
14
|
+
: '<span class="badge transparent">TRANSPARENT</span>';
|
|
13
15
|
return `<!DOCTYPE html>
|
|
14
16
|
<html lang="en">
|
|
15
17
|
<head>
|
|
@@ -21,17 +23,23 @@ function renderDashboard(metrics, paused) {
|
|
|
21
23
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; }
|
|
22
24
|
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
23
25
|
header { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid #21262d; margin-bottom: 24px; }
|
|
24
|
-
h1 { font-size: 20px; font-weight: 600; }
|
|
26
|
+
h1 { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
|
25
27
|
h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #8b949e; }
|
|
26
|
-
.badge { padding: 4px 12px; border-radius: 12px; font-size:
|
|
28
|
+
.badge { padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
27
29
|
.badge.running { background: #238636; color: #fff; }
|
|
28
30
|
.badge.paused { background: #d29922; color: #000; }
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
31
|
+
.badge.opt { background: #1f6feb; color: #fff; }
|
|
32
|
+
.badge.transparent { background: #30363d; color: #8b949e; }
|
|
33
|
+
.badge.on { background: #238636; color: #fff; }
|
|
34
|
+
.badge.off { background: #30363d; color: #8b949e; }
|
|
35
|
+
.controls { display: flex; gap: 8px; align-items: center; }
|
|
36
|
+
.btn { padding: 6px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; font-size: 13px; transition: all 0.15s; }
|
|
37
|
+
.btn:hover { background: #30363d; border-color: #484f58; }
|
|
32
38
|
.btn.primary { background: #238636; border-color: #238636; }
|
|
39
|
+
.btn.primary:hover { background: #2ea043; }
|
|
33
40
|
.btn.warn { background: #d29922; border-color: #d29922; color: #000; }
|
|
34
|
-
.
|
|
41
|
+
.btn.danger { background: #da3633; border-color: #da3633; }
|
|
42
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
35
43
|
.card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; }
|
|
36
44
|
.card .value { font-size: 32px; font-weight: 700; color: #58a6ff; }
|
|
37
45
|
.card .label { font-size: 13px; color: #8b949e; margin-top: 4px; }
|
|
@@ -47,20 +55,46 @@ function renderDashboard(metrics, paused) {
|
|
|
47
55
|
.tab.active { color: #e1e4e8; border-bottom-color: #58a6ff; }
|
|
48
56
|
.tab-content { display: none; }
|
|
49
57
|
.tab-content.active { display: block; }
|
|
58
|
+
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
59
|
+
.setting { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px; display: flex; justify-content: space-between; align-items: center; }
|
|
60
|
+
.setting-info { flex: 1; }
|
|
61
|
+
.setting-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
62
|
+
.setting-desc { font-size: 12px; color: #8b949e; }
|
|
63
|
+
.toggle { position: relative; width: 44px; height: 24px; cursor: pointer; }
|
|
64
|
+
.toggle input { display: none; }
|
|
65
|
+
.toggle .slider { position: absolute; inset: 0; background: #30363d; border-radius: 12px; transition: 0.2s; }
|
|
66
|
+
.toggle .slider:before { content: ''; position: absolute; width: 18px; height: 18px; left: 3px; top: 3px; background: #8b949e; border-radius: 50%; transition: 0.2s; }
|
|
67
|
+
.toggle input:checked + .slider { background: #238636; }
|
|
68
|
+
.toggle input:checked + .slider:before { transform: translateX(20px); background: #fff; }
|
|
69
|
+
.status-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
70
|
+
.status-pill { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #8b949e; }
|
|
71
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
72
|
+
.dot.green { background: #3fb950; }
|
|
73
|
+
.dot.yellow { background: #d29922; }
|
|
74
|
+
.dot.red { background: #f85149; }
|
|
75
|
+
.dot.gray { background: #484f58; }
|
|
50
76
|
.refresh-note { font-size: 11px; color: #484f58; text-align: right; margin-top: 8px; }
|
|
51
77
|
</style>
|
|
52
78
|
</head>
|
|
53
79
|
<body>
|
|
54
80
|
<div class="container">
|
|
55
81
|
<header>
|
|
56
|
-
<h1>SmartContext Proxy ${
|
|
82
|
+
<h1>SmartContext Proxy ${stateBadge} ${modeBadge}</h1>
|
|
57
83
|
<div class="controls">
|
|
58
|
-
${paused
|
|
84
|
+
${state.paused
|
|
59
85
|
? '<button class="btn primary" onclick="api(\'/_sc/resume\')">Resume</button>'
|
|
60
86
|
: '<button class="btn warn" onclick="api(\'/_sc/pause\')">Pause</button>'}
|
|
61
87
|
</div>
|
|
62
88
|
</header>
|
|
63
89
|
|
|
90
|
+
<div class="status-row">
|
|
91
|
+
<div class="status-pill"><div class="dot ${state.caInstalled ? 'green' : 'red'}"></div>CA Certificate</div>
|
|
92
|
+
<div class="status-pill"><div class="dot ${state.systemProxyActive ? 'green' : 'gray'}"></div>System Proxy</div>
|
|
93
|
+
<div class="status-pill"><div class="dot ${state.mode === 'optimizing' ? 'green' : 'gray'}"></div>Context Optimization</div>
|
|
94
|
+
<div class="status-pill"><div class="dot ${state.abTestEnabled ? 'yellow' : 'gray'}"></div>A/B Test</div>
|
|
95
|
+
<div class="status-pill"><div class="dot ${state.proxyType === 'connect' ? 'green' : 'gray'}"></div>${state.proxyType === 'connect' ? 'Transparent' : 'Legacy'} Mode</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
64
98
|
<div class="grid">
|
|
65
99
|
<div class="card savings">
|
|
66
100
|
<div class="value">$${savingsAmount}</div>
|
|
@@ -81,9 +115,10 @@ function renderDashboard(metrics, paused) {
|
|
|
81
115
|
</div>
|
|
82
116
|
|
|
83
117
|
<div class="tab-bar">
|
|
84
|
-
<div class="tab active" onclick="switchTab('feed')">Live Feed</div>
|
|
85
|
-
<div class="tab" onclick="switchTab('providers')">By Provider</div>
|
|
86
|
-
<div class="tab" onclick="switchTab('models')">By Model</div>
|
|
118
|
+
<div class="tab active" onclick="switchTab('feed',this)">Live Feed</div>
|
|
119
|
+
<div class="tab" onclick="switchTab('providers',this)">By Provider</div>
|
|
120
|
+
<div class="tab" onclick="switchTab('models',this)">By Model</div>
|
|
121
|
+
<div class="tab" onclick="switchTab('settings',this)">Settings</div>
|
|
87
122
|
</div>
|
|
88
123
|
|
|
89
124
|
<div id="tab-feed" class="tab-content active">
|
|
@@ -113,12 +148,7 @@ function renderDashboard(metrics, paused) {
|
|
|
113
148
|
<thead><tr><th>Provider</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
|
|
114
149
|
<tbody>
|
|
115
150
|
${Object.entries(stats.byProvider).map(([name, s]) => `
|
|
116
|
-
<tr>
|
|
117
|
-
<td>${name}</td>
|
|
118
|
-
<td class="mono">${s.requests}</td>
|
|
119
|
-
<td class="mono">${formatTokens(s.tokensSaved)}</td>
|
|
120
|
-
<td class="savings-pct">${s.savingsPercent}%</td>
|
|
121
|
-
</tr>
|
|
151
|
+
<tr><td>${name}</td><td class="mono">${s.requests}</td><td class="mono">${formatTokens(s.tokensSaved)}</td><td class="savings-pct">${s.savingsPercent}%</td></tr>
|
|
122
152
|
`).join('')}
|
|
123
153
|
</tbody>
|
|
124
154
|
</table>
|
|
@@ -130,31 +160,88 @@ function renderDashboard(metrics, paused) {
|
|
|
130
160
|
<thead><tr><th>Model</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
|
|
131
161
|
<tbody>
|
|
132
162
|
${Object.entries(stats.byModel).map(([name, s]) => `
|
|
133
|
-
<tr>
|
|
134
|
-
<td>${name}</td>
|
|
135
|
-
<td class="mono">${s.requests}</td>
|
|
136
|
-
<td class="mono">${formatTokens(s.tokensSaved)}</td>
|
|
137
|
-
<td class="savings-pct">${s.savingsPercent}%</td>
|
|
138
|
-
</tr>
|
|
163
|
+
<tr><td>${name}</td><td class="mono">${s.requests}</td><td class="mono">${formatTokens(s.tokensSaved)}</td><td class="savings-pct">${s.savingsPercent}%</td></tr>
|
|
139
164
|
`).join('')}
|
|
140
165
|
</tbody>
|
|
141
166
|
</table>
|
|
142
167
|
</div>
|
|
143
168
|
|
|
144
|
-
<div
|
|
169
|
+
<div id="tab-settings" class="tab-content">
|
|
170
|
+
<h2>Controls</h2>
|
|
171
|
+
<div class="settings-grid">
|
|
172
|
+
<div class="setting">
|
|
173
|
+
<div class="setting-info">
|
|
174
|
+
<div class="setting-name">Context Optimization</div>
|
|
175
|
+
<div class="setting-desc">Optimize LLM context windows to reduce token usage</div>
|
|
176
|
+
</div>
|
|
177
|
+
<label class="toggle">
|
|
178
|
+
<input type="checkbox" ${!state.paused ? 'checked' : ''} onchange="api(this.checked ? '/_sc/resume' : '/_sc/pause')">
|
|
179
|
+
<span class="slider"></span>
|
|
180
|
+
</label>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="setting">
|
|
184
|
+
<div class="setting-info">
|
|
185
|
+
<div class="setting-name">A/B Test Mode</div>
|
|
186
|
+
<div class="setting-desc">Send each request twice to compare quality</div>
|
|
187
|
+
</div>
|
|
188
|
+
<label class="toggle">
|
|
189
|
+
<input type="checkbox" ${state.abTestEnabled ? 'checked' : ''} onchange="api(this.checked ? '/_sc/ab-test/enable' : '/_sc/ab-test/disable')">
|
|
190
|
+
<span class="slider"></span>
|
|
191
|
+
</label>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div class="setting">
|
|
195
|
+
<div class="setting-info">
|
|
196
|
+
<div class="setting-name">Debug Headers</div>
|
|
197
|
+
<div class="setting-desc">Add X-SmartContext-* headers to responses</div>
|
|
198
|
+
</div>
|
|
199
|
+
<label class="toggle">
|
|
200
|
+
<input type="checkbox" ${state.debugHeaders ? 'checked' : ''} onchange="api(this.checked ? '/_sc/debug-headers/enable' : '/_sc/debug-headers/disable')">
|
|
201
|
+
<span class="slider"></span>
|
|
202
|
+
</label>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="setting">
|
|
206
|
+
<div class="setting-info">
|
|
207
|
+
<div class="setting-name">System Proxy</div>
|
|
208
|
+
<div class="setting-desc">Auto-intercept all LLM traffic system-wide</div>
|
|
209
|
+
</div>
|
|
210
|
+
<label class="toggle">
|
|
211
|
+
<input type="checkbox" ${state.systemProxyActive ? 'checked' : ''} onchange="api(this.checked ? '/_sc/system-proxy/enable' : '/_sc/system-proxy/disable')">
|
|
212
|
+
<span class="slider"></span>
|
|
213
|
+
</label>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div class="refresh-note">v0.2.0 | Uptime: ${uptimeStr} | Auto-refresh: 5s</div>
|
|
145
219
|
</div>
|
|
146
220
|
<script>
|
|
147
|
-
function switchTab(name) {
|
|
221
|
+
function switchTab(name, el) {
|
|
148
222
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
149
223
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
150
224
|
document.getElementById('tab-' + name).classList.add('active');
|
|
151
|
-
|
|
225
|
+
if (el) el.classList.add('active');
|
|
226
|
+
location.hash = name;
|
|
152
227
|
}
|
|
153
228
|
async function api(path) {
|
|
154
229
|
await fetch(path, { method: 'POST' });
|
|
155
230
|
location.reload();
|
|
156
231
|
}
|
|
157
|
-
|
|
232
|
+
// Restore tab from URL hash
|
|
233
|
+
(function() {
|
|
234
|
+
var hash = location.hash.replace('#', '');
|
|
235
|
+
if (hash && document.getElementById('tab-' + hash)) {
|
|
236
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
237
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
238
|
+
document.getElementById('tab-' + hash).classList.add('active');
|
|
239
|
+
document.querySelectorAll('.tab').forEach(t => {
|
|
240
|
+
if (t.getAttribute('onclick') && t.getAttribute('onclick').indexOf("'" + hash + "'") !== -1) t.classList.add('active');
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
setTimeout(() => { var h = location.hash; location.reload(); if (h) location.hash = h; }, 5000);
|
|
158
245
|
</script>
|
|
159
246
|
</body>
|
|
160
247
|
</html>`;
|
|
@@ -174,9 +261,7 @@ function formatDuration(ms) {
|
|
|
174
261
|
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
175
262
|
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
176
263
|
}
|
|
177
|
-
/** Rough cost estimate based on Anthropic/OpenAI pricing */
|
|
178
264
|
function estimateCostSaved(tokensSaved) {
|
|
179
|
-
// Assume avg $15/1M input tokens (Opus pricing)
|
|
180
265
|
const cost = (tokensSaved / 1000000) * 15;
|
|
181
266
|
return cost.toFixed(2);
|
|
182
267
|
}
|
package/package.json
CHANGED
|
@@ -7,7 +7,10 @@ import type { SmartContextConfig } from '../config/schema.js';
|
|
|
7
7
|
import type { ProviderAdapter } from '../providers/types.js';
|
|
8
8
|
import { ContextOptimizer } from '../context/optimizer.js';
|
|
9
9
|
import { MetricsCollector } from '../metrics/collector.js';
|
|
10
|
-
import { renderDashboard } from '../ui/dashboard.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';
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* HTTP CONNECT proxy that transparently intercepts LLM traffic.
|
|
@@ -22,6 +25,8 @@ export class ConnectProxy {
|
|
|
22
25
|
private optimizer: ContextOptimizer | null = null;
|
|
23
26
|
private adapters = new Map<string, ProviderAdapter>();
|
|
24
27
|
private paused = false;
|
|
28
|
+
private debugHeaders = false;
|
|
29
|
+
private systemProxyActive = false;
|
|
25
30
|
private requestCounter = { value: 0 };
|
|
26
31
|
private config: SmartContextConfig;
|
|
27
32
|
|
|
@@ -80,7 +85,7 @@ export class ConnectProxy {
|
|
|
80
85
|
// Dashboard
|
|
81
86
|
if (path === '/' && method === 'GET') {
|
|
82
87
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
83
|
-
res.end(renderDashboard(this.metrics, this.
|
|
88
|
+
res.end(renderDashboard(this.metrics, this.getDashboardState()));
|
|
84
89
|
return;
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -115,7 +120,7 @@ export class ConnectProxy {
|
|
|
115
120
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
private handleAPI(path: string, method: string, req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
123
|
+
private async handleAPI(path: string, method: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
119
124
|
res.setHeader('Content-Type', 'application/json');
|
|
120
125
|
|
|
121
126
|
switch (path) {
|
|
@@ -141,12 +146,71 @@ export class ConnectProxy {
|
|
|
141
146
|
this.paused = false;
|
|
142
147
|
res.end(JSON.stringify({ ok: true, state: 'running' }));
|
|
143
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;
|
|
144
193
|
default:
|
|
145
194
|
res.writeHead(404);
|
|
146
195
|
res.end(JSON.stringify({ error: `Unknown: ${path}` }));
|
|
147
196
|
}
|
|
148
197
|
}
|
|
149
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
|
+
|
|
150
214
|
private generatePAC(): string {
|
|
151
215
|
const { getLLMHostnames } = require('./classifier.js');
|
|
152
216
|
const hosts = getLLMHostnames();
|
package/src/proxy/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|