smartcontext-proxy 0.1.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.
- package/PLAN.md +406 -0
- package/PROGRESS.md +60 -0
- package/README.md +99 -0
- package/SPEC.md +915 -0
- package/adapters/openclaw/embedding.d.ts +8 -0
- package/adapters/openclaw/embedding.js +16 -0
- package/adapters/openclaw/embedding.ts +15 -0
- package/adapters/openclaw/index.d.ts +18 -0
- package/adapters/openclaw/index.js +42 -0
- package/adapters/openclaw/index.ts +43 -0
- package/adapters/openclaw/session-importer.d.ts +22 -0
- package/adapters/openclaw/session-importer.js +99 -0
- package/adapters/openclaw/session-importer.ts +105 -0
- package/adapters/openclaw/storage.d.ts +26 -0
- package/adapters/openclaw/storage.js +177 -0
- package/adapters/openclaw/storage.ts +183 -0
- package/dist/adapters/openclaw/embedding.d.ts +8 -0
- package/dist/adapters/openclaw/embedding.js +16 -0
- package/dist/adapters/openclaw/index.d.ts +18 -0
- package/dist/adapters/openclaw/index.js +42 -0
- package/dist/adapters/openclaw/session-importer.d.ts +22 -0
- package/dist/adapters/openclaw/session-importer.js +99 -0
- package/dist/adapters/openclaw/storage.d.ts +26 -0
- package/dist/adapters/openclaw/storage.js +177 -0
- package/dist/config/auto-detect.d.ts +3 -0
- package/dist/config/auto-detect.js +48 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +28 -0
- package/dist/config/schema.d.ts +30 -0
- package/dist/config/schema.js +3 -0
- package/dist/context/budget.d.ts +25 -0
- package/dist/context/budget.js +85 -0
- package/dist/context/canonical.d.ts +39 -0
- package/dist/context/canonical.js +12 -0
- package/dist/context/chunker.d.ts +9 -0
- package/dist/context/chunker.js +148 -0
- package/dist/context/optimizer.d.ts +31 -0
- package/dist/context/optimizer.js +163 -0
- package/dist/context/retriever.d.ts +29 -0
- package/dist/context/retriever.js +103 -0
- package/dist/daemon/process.d.ts +6 -0
- package/dist/daemon/process.js +76 -0
- package/dist/daemon/service.d.ts +2 -0
- package/dist/daemon/service.js +99 -0
- package/dist/embedding/ollama.d.ts +11 -0
- package/dist/embedding/ollama.js +72 -0
- package/dist/embedding/types.d.ts +6 -0
- package/dist/embedding/types.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +190 -0
- package/dist/metrics/collector.d.ts +43 -0
- package/dist/metrics/collector.js +72 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +109 -0
- package/dist/providers/google.d.ts +13 -0
- package/dist/providers/google.js +40 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.js +82 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +115 -0
- package/dist/providers/types.d.ts +18 -0
- package/dist/providers/types.js +3 -0
- package/dist/proxy/router.d.ts +12 -0
- package/dist/proxy/router.js +46 -0
- package/dist/proxy/server.d.ts +25 -0
- package/dist/proxy/server.js +265 -0
- package/dist/proxy/stream.d.ts +8 -0
- package/dist/proxy/stream.js +32 -0
- package/dist/src/config/auto-detect.d.ts +3 -0
- package/dist/src/config/auto-detect.js +48 -0
- package/dist/src/config/defaults.d.ts +2 -0
- package/dist/src/config/defaults.js +28 -0
- package/dist/src/config/schema.d.ts +30 -0
- package/dist/src/config/schema.js +3 -0
- package/dist/src/context/budget.d.ts +25 -0
- package/dist/src/context/budget.js +85 -0
- package/dist/src/context/canonical.d.ts +39 -0
- package/dist/src/context/canonical.js +12 -0
- package/dist/src/context/chunker.d.ts +9 -0
- package/dist/src/context/chunker.js +148 -0
- package/dist/src/context/optimizer.d.ts +31 -0
- package/dist/src/context/optimizer.js +163 -0
- package/dist/src/context/retriever.d.ts +29 -0
- package/dist/src/context/retriever.js +103 -0
- package/dist/src/daemon/process.d.ts +6 -0
- package/dist/src/daemon/process.js +76 -0
- package/dist/src/daemon/service.d.ts +2 -0
- package/dist/src/daemon/service.js +99 -0
- package/dist/src/embedding/ollama.d.ts +11 -0
- package/dist/src/embedding/ollama.js +72 -0
- package/dist/src/embedding/types.d.ts +6 -0
- package/dist/src/embedding/types.js +3 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +190 -0
- package/dist/src/metrics/collector.d.ts +43 -0
- package/dist/src/metrics/collector.js +72 -0
- package/dist/src/providers/anthropic.d.ts +15 -0
- package/dist/src/providers/anthropic.js +109 -0
- package/dist/src/providers/google.d.ts +13 -0
- package/dist/src/providers/google.js +40 -0
- package/dist/src/providers/ollama.d.ts +13 -0
- package/dist/src/providers/ollama.js +82 -0
- package/dist/src/providers/openai.d.ts +15 -0
- package/dist/src/providers/openai.js +115 -0
- package/dist/src/providers/types.d.ts +18 -0
- package/dist/src/providers/types.js +3 -0
- package/dist/src/proxy/router.d.ts +12 -0
- package/dist/src/proxy/router.js +46 -0
- package/dist/src/proxy/server.d.ts +25 -0
- package/dist/src/proxy/server.js +265 -0
- package/dist/src/proxy/stream.d.ts +8 -0
- package/dist/src/proxy/stream.js +32 -0
- package/dist/src/storage/lancedb.d.ts +21 -0
- package/dist/src/storage/lancedb.js +158 -0
- package/dist/src/storage/types.d.ts +52 -0
- package/dist/src/storage/types.js +3 -0
- package/dist/src/test/context.test.d.ts +1 -0
- package/dist/src/test/context.test.js +141 -0
- package/dist/src/test/dashboard.test.d.ts +1 -0
- package/dist/src/test/dashboard.test.js +85 -0
- package/dist/src/test/proxy.test.d.ts +1 -0
- package/dist/src/test/proxy.test.js +188 -0
- package/dist/src/ui/dashboard.d.ts +2 -0
- package/dist/src/ui/dashboard.js +183 -0
- package/dist/storage/lancedb.d.ts +21 -0
- package/dist/storage/lancedb.js +158 -0
- package/dist/storage/types.d.ts +52 -0
- package/dist/storage/types.js +3 -0
- package/dist/test/context.test.d.ts +1 -0
- package/dist/test/context.test.js +141 -0
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +85 -0
- package/dist/test/proxy.test.d.ts +1 -0
- package/dist/test/proxy.test.js +188 -0
- package/dist/ui/dashboard.d.ts +2 -0
- package/dist/ui/dashboard.js +183 -0
- package/package.json +38 -0
- package/src/config/auto-detect.ts +51 -0
- package/src/config/defaults.ts +26 -0
- package/src/config/schema.ts +33 -0
- package/src/context/budget.ts +126 -0
- package/src/context/canonical.ts +50 -0
- package/src/context/chunker.ts +165 -0
- package/src/context/optimizer.ts +201 -0
- package/src/context/retriever.ts +123 -0
- package/src/daemon/process.ts +70 -0
- package/src/daemon/service.ts +103 -0
- package/src/embedding/ollama.ts +68 -0
- package/src/embedding/types.ts +6 -0
- package/src/index.ts +176 -0
- package/src/metrics/collector.ts +114 -0
- package/src/providers/anthropic.ts +117 -0
- package/src/providers/google.ts +42 -0
- package/src/providers/ollama.ts +87 -0
- package/src/providers/openai.ts +127 -0
- package/src/providers/types.ts +20 -0
- package/src/proxy/router.ts +48 -0
- package/src/proxy/server.ts +315 -0
- package/src/proxy/stream.ts +39 -0
- package/src/storage/lancedb.ts +169 -0
- package/src/storage/types.ts +47 -0
- package/src/test/context.test.ts +165 -0
- package/src/test/dashboard.test.ts +94 -0
- package/src/test/proxy.test.ts +218 -0
- package/src/ui/dashboard.ts +184 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
const node_test_1 = require("node:test");
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
9
|
+
const server_js_1 = require("../proxy/server.js");
|
|
10
|
+
const auto_detect_js_1 = require("../config/auto-detect.js");
|
|
11
|
+
/** Create a mock LLM provider server */
|
|
12
|
+
function createMockProvider(port) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
15
|
+
let body = '';
|
|
16
|
+
req.on('data', (chunk) => (body += chunk));
|
|
17
|
+
req.on('end', () => {
|
|
18
|
+
const parsed = JSON.parse(body);
|
|
19
|
+
if (parsed.stream) {
|
|
20
|
+
// SSE streaming response
|
|
21
|
+
res.writeHead(200, {
|
|
22
|
+
'Content-Type': 'text/event-stream',
|
|
23
|
+
'Cache-Control': 'no-cache',
|
|
24
|
+
});
|
|
25
|
+
const events = [
|
|
26
|
+
'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n',
|
|
27
|
+
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n',
|
|
28
|
+
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" there"}}\n\n',
|
|
29
|
+
'data: {"type":"content_block_stop","index":0}\n\n',
|
|
30
|
+
'data: {"type":"message_stop"}\n\n',
|
|
31
|
+
];
|
|
32
|
+
let i = 0;
|
|
33
|
+
const interval = setInterval(() => {
|
|
34
|
+
if (i < events.length) {
|
|
35
|
+
res.write(events[i]);
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
clearInterval(interval);
|
|
40
|
+
res.end();
|
|
41
|
+
}
|
|
42
|
+
}, 10);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Non-streaming response
|
|
46
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({
|
|
48
|
+
id: 'msg_test',
|
|
49
|
+
type: 'message',
|
|
50
|
+
role: 'assistant',
|
|
51
|
+
content: [{ type: 'text', text: 'Hello from mock provider!' }],
|
|
52
|
+
model: parsed.model || 'test-model',
|
|
53
|
+
stop_reason: 'end_turn',
|
|
54
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
server.listen(port, '127.0.0.1', () => resolve(server));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function httpRequest(url, options, body) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const req = node_http_1.default.request(url, options, (res) => {
|
|
65
|
+
let data = '';
|
|
66
|
+
res.on('data', (chunk) => (data += chunk));
|
|
67
|
+
res.on('end', () => resolve({ status: res.statusCode || 0, headers: res.headers, body: data }));
|
|
68
|
+
});
|
|
69
|
+
req.on('error', reject);
|
|
70
|
+
if (body)
|
|
71
|
+
req.write(body);
|
|
72
|
+
req.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
(0, node_test_1.describe)('SmartContext Proxy', () => {
|
|
76
|
+
let proxy;
|
|
77
|
+
let mockServer;
|
|
78
|
+
const PROXY_PORT = 14800;
|
|
79
|
+
const MOCK_PORT = 14801;
|
|
80
|
+
(0, node_test_1.before)(async () => {
|
|
81
|
+
// Start mock provider
|
|
82
|
+
mockServer = await createMockProvider(MOCK_PORT);
|
|
83
|
+
// Start proxy pointing to mock provider
|
|
84
|
+
const config = (0, auto_detect_js_1.buildConfig)({
|
|
85
|
+
proxy: { port: PROXY_PORT, host: '127.0.0.1' },
|
|
86
|
+
});
|
|
87
|
+
// Override anthropic baseUrl to point to mock
|
|
88
|
+
config.providers['anthropic'] = {
|
|
89
|
+
apiKey: 'test-key',
|
|
90
|
+
baseUrl: `http://127.0.0.1:${MOCK_PORT}`,
|
|
91
|
+
};
|
|
92
|
+
config.providers['openai'] = {
|
|
93
|
+
apiKey: 'test-key',
|
|
94
|
+
baseUrl: `http://127.0.0.1:${MOCK_PORT}`,
|
|
95
|
+
};
|
|
96
|
+
config.logging.level = 'error'; // quiet during tests
|
|
97
|
+
proxy = new server_js_1.ProxyServer(config);
|
|
98
|
+
await proxy.start();
|
|
99
|
+
});
|
|
100
|
+
(0, node_test_1.after)(async () => {
|
|
101
|
+
await proxy.stop();
|
|
102
|
+
mockServer.close();
|
|
103
|
+
});
|
|
104
|
+
(0, node_test_1.it)('health endpoint returns ok', async () => {
|
|
105
|
+
const res = await httpRequest(`http://127.0.0.1:${PROXY_PORT}/health`, { method: 'GET' });
|
|
106
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
107
|
+
const data = JSON.parse(res.body);
|
|
108
|
+
node_assert_1.default.strictEqual(data.ok, true);
|
|
109
|
+
});
|
|
110
|
+
(0, node_test_1.it)('returns 404 for unknown provider', async () => {
|
|
111
|
+
const res = await httpRequest(`http://127.0.0.1:${PROXY_PORT}/v1/unknown/test`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
}, '{}');
|
|
115
|
+
node_assert_1.default.strictEqual(res.status, 404);
|
|
116
|
+
});
|
|
117
|
+
(0, node_test_1.it)('returns 405 for GET on provider path', async () => {
|
|
118
|
+
const res = await httpRequest(`http://127.0.0.1:${PROXY_PORT}/v1/anthropic/v1/messages`, {
|
|
119
|
+
method: 'GET',
|
|
120
|
+
});
|
|
121
|
+
node_assert_1.default.strictEqual(res.status, 405);
|
|
122
|
+
});
|
|
123
|
+
(0, node_test_1.it)('forwards Anthropic non-streaming request correctly', async () => {
|
|
124
|
+
const body = JSON.stringify({
|
|
125
|
+
model: 'claude-haiku-4-5-20251001',
|
|
126
|
+
max_tokens: 50,
|
|
127
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
128
|
+
});
|
|
129
|
+
const res = await httpRequest(`http://127.0.0.1:${PROXY_PORT}/v1/anthropic/v1/messages`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'x-api-key': 'test-key',
|
|
134
|
+
'anthropic-version': '2023-06-01',
|
|
135
|
+
},
|
|
136
|
+
}, body);
|
|
137
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
138
|
+
const data = JSON.parse(res.body);
|
|
139
|
+
node_assert_1.default.strictEqual(data.content[0].text, 'Hello from mock provider!');
|
|
140
|
+
node_assert_1.default.strictEqual(data.role, 'assistant');
|
|
141
|
+
});
|
|
142
|
+
(0, node_test_1.it)('forwards Anthropic streaming request correctly', async () => {
|
|
143
|
+
const body = JSON.stringify({
|
|
144
|
+
model: 'claude-haiku-4-5-20251001',
|
|
145
|
+
max_tokens: 50,
|
|
146
|
+
stream: true,
|
|
147
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
148
|
+
});
|
|
149
|
+
const res = await httpRequest(`http://127.0.0.1:${PROXY_PORT}/v1/anthropic/v1/messages`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'x-api-key': 'test-key',
|
|
154
|
+
'anthropic-version': '2023-06-01',
|
|
155
|
+
},
|
|
156
|
+
}, body);
|
|
157
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
158
|
+
node_assert_1.default.ok(res.body.includes('Hello'));
|
|
159
|
+
node_assert_1.default.ok(res.body.includes('there'));
|
|
160
|
+
node_assert_1.default.ok(res.body.includes('message_stop'));
|
|
161
|
+
});
|
|
162
|
+
(0, node_test_1.it)('forwards OpenAI request correctly', async () => {
|
|
163
|
+
const body = JSON.stringify({
|
|
164
|
+
model: 'gpt-4o',
|
|
165
|
+
messages: [
|
|
166
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
167
|
+
{ role: 'user', content: 'Hello' },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
const res = await httpRequest(`http://127.0.0.1:${PROXY_PORT}/v1/openai/v1/chat/completions`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
'Authorization': 'Bearer test-key',
|
|
175
|
+
},
|
|
176
|
+
}, body);
|
|
177
|
+
node_assert_1.default.strictEqual(res.status, 200);
|
|
178
|
+
const data = JSON.parse(res.body);
|
|
179
|
+
node_assert_1.default.ok(data.content);
|
|
180
|
+
});
|
|
181
|
+
(0, node_test_1.it)('auto-detect finds providers from config', () => {
|
|
182
|
+
const providers = proxy.getProviderNames();
|
|
183
|
+
node_assert_1.default.ok(providers.includes('anthropic'));
|
|
184
|
+
node_assert_1.default.ok(providers.includes('openai'));
|
|
185
|
+
node_assert_1.default.ok(providers.includes('ollama'));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=proxy.test.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDashboard = renderDashboard;
|
|
4
|
+
function renderDashboard(metrics, paused) {
|
|
5
|
+
const stats = metrics.getStats();
|
|
6
|
+
const recent = metrics.getRecent(20);
|
|
7
|
+
const uptime = metrics.getUptime();
|
|
8
|
+
const uptimeStr = formatDuration(uptime);
|
|
9
|
+
const statusBadge = paused
|
|
10
|
+
? '<span class="badge paused">PAUSED</span>'
|
|
11
|
+
: '<span class="badge running">RUNNING</span>';
|
|
12
|
+
const savingsAmount = estimateCostSaved(stats.totalOriginalTokens - stats.totalOptimizedTokens);
|
|
13
|
+
return `<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>SmartContext Proxy</title>
|
|
19
|
+
<style>
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; }
|
|
22
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
23
|
+
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; }
|
|
25
|
+
h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #8b949e; }
|
|
26
|
+
.badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
|
27
|
+
.badge.running { background: #238636; color: #fff; }
|
|
28
|
+
.badge.paused { background: #d29922; color: #000; }
|
|
29
|
+
.controls { display: flex; gap: 8px; }
|
|
30
|
+
.btn { padding: 6px 16px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #e1e4e8; cursor: pointer; font-size: 13px; }
|
|
31
|
+
.btn:hover { background: #30363d; }
|
|
32
|
+
.btn.primary { background: #238636; border-color: #238636; }
|
|
33
|
+
.btn.warn { background: #d29922; border-color: #d29922; color: #000; }
|
|
34
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
35
|
+
.card { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 20px; }
|
|
36
|
+
.card .value { font-size: 32px; font-weight: 700; color: #58a6ff; }
|
|
37
|
+
.card .label { font-size: 13px; color: #8b949e; margin-top: 4px; }
|
|
38
|
+
.card.savings .value { color: #3fb950; }
|
|
39
|
+
table { width: 100%; border-collapse: collapse; }
|
|
40
|
+
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #21262d; font-size: 13px; }
|
|
41
|
+
th { color: #8b949e; font-weight: 600; }
|
|
42
|
+
.mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
43
|
+
.savings-pct { color: #3fb950; font-weight: 600; }
|
|
44
|
+
.pass { color: #8b949e; }
|
|
45
|
+
.tab-bar { display: flex; gap: 0; border-bottom: 1px solid #21262d; margin-bottom: 16px; }
|
|
46
|
+
.tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; color: #8b949e; font-size: 14px; }
|
|
47
|
+
.tab.active { color: #e1e4e8; border-bottom-color: #58a6ff; }
|
|
48
|
+
.tab-content { display: none; }
|
|
49
|
+
.tab-content.active { display: block; }
|
|
50
|
+
.refresh-note { font-size: 11px; color: #484f58; text-align: right; margin-top: 8px; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div class="container">
|
|
55
|
+
<header>
|
|
56
|
+
<h1>SmartContext Proxy ${statusBadge}</h1>
|
|
57
|
+
<div class="controls">
|
|
58
|
+
${paused
|
|
59
|
+
? '<button class="btn primary" onclick="api(\'/_sc/resume\')">Resume</button>'
|
|
60
|
+
: '<button class="btn warn" onclick="api(\'/_sc/pause\')">Pause</button>'}
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<div class="grid">
|
|
65
|
+
<div class="card savings">
|
|
66
|
+
<div class="value">$${savingsAmount}</div>
|
|
67
|
+
<div class="label">Estimated Saved</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="card">
|
|
70
|
+
<div class="value">${stats.totalRequests}</div>
|
|
71
|
+
<div class="label">Total Requests</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="value">${stats.totalSavingsPercent}%</div>
|
|
75
|
+
<div class="label">Avg Token Savings</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="card">
|
|
78
|
+
<div class="value">${stats.avgLatencyOverheadMs}ms</div>
|
|
79
|
+
<div class="label">Avg Latency Overhead</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<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>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div id="tab-feed" class="tab-content active">
|
|
90
|
+
<h2>Recent Requests</h2>
|
|
91
|
+
<table>
|
|
92
|
+
<thead><tr><th>ID</th><th>Time</th><th>Provider/Model</th><th>Original</th><th>Optimized</th><th>Savings</th><th>Chunks</th><th>Latency</th></tr></thead>
|
|
93
|
+
<tbody>
|
|
94
|
+
${recent.reverse().map((r) => `
|
|
95
|
+
<tr>
|
|
96
|
+
<td class="mono">#${r.id}</td>
|
|
97
|
+
<td class="mono">${new Date(r.timestamp).toLocaleTimeString()}</td>
|
|
98
|
+
<td>${r.provider}/${r.model}</td>
|
|
99
|
+
<td class="mono">${formatTokens(r.originalTokens)}</td>
|
|
100
|
+
<td class="mono">${formatTokens(r.optimizedTokens)}</td>
|
|
101
|
+
<td class="${r.passThrough ? 'pass' : 'savings-pct'}">${r.passThrough ? 'pass' : `-${r.savingsPercent}%`}</td>
|
|
102
|
+
<td class="mono">${r.chunksRetrieved}</td>
|
|
103
|
+
<td class="mono">${r.latencyOverheadMs}ms</td>
|
|
104
|
+
</tr>
|
|
105
|
+
`).join('')}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div id="tab-providers" class="tab-content">
|
|
111
|
+
<h2>Savings by Provider</h2>
|
|
112
|
+
<table>
|
|
113
|
+
<thead><tr><th>Provider</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
|
|
114
|
+
<tbody>
|
|
115
|
+
${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>
|
|
122
|
+
`).join('')}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div id="tab-models" class="tab-content">
|
|
128
|
+
<h2>Savings by Model</h2>
|
|
129
|
+
<table>
|
|
130
|
+
<thead><tr><th>Model</th><th>Requests</th><th>Tokens Saved</th><th>Savings %</th></tr></thead>
|
|
131
|
+
<tbody>
|
|
132
|
+
${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>
|
|
139
|
+
`).join('')}
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="refresh-note">Uptime: ${uptimeStr} | Auto-refresh: 10s</div>
|
|
145
|
+
</div>
|
|
146
|
+
<script>
|
|
147
|
+
function switchTab(name) {
|
|
148
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
149
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
150
|
+
document.getElementById('tab-' + name).classList.add('active');
|
|
151
|
+
event.target.classList.add('active');
|
|
152
|
+
}
|
|
153
|
+
async function api(path) {
|
|
154
|
+
await fetch(path, { method: 'POST' });
|
|
155
|
+
location.reload();
|
|
156
|
+
}
|
|
157
|
+
setTimeout(() => location.reload(), 10000);
|
|
158
|
+
</script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>`;
|
|
161
|
+
}
|
|
162
|
+
function formatTokens(n) {
|
|
163
|
+
if (n >= 1000000)
|
|
164
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
165
|
+
if (n >= 1000)
|
|
166
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
167
|
+
return String(n);
|
|
168
|
+
}
|
|
169
|
+
function formatDuration(ms) {
|
|
170
|
+
const s = Math.floor(ms / 1000);
|
|
171
|
+
if (s < 60)
|
|
172
|
+
return `${s}s`;
|
|
173
|
+
if (s < 3600)
|
|
174
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
175
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
176
|
+
}
|
|
177
|
+
/** Rough cost estimate based on Anthropic/OpenAI pricing */
|
|
178
|
+
function estimateCostSaved(tokensSaved) {
|
|
179
|
+
// Assume avg $15/1M input tokens (Opus pricing)
|
|
180
|
+
const cost = (tokensSaved / 1000000) * 15;
|
|
181
|
+
return cost.toFixed(2);
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=dashboard.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smartcontext-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Intelligent context window optimization proxy for LLM APIs",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"smartcontext-proxy": "dist/src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "node --test dist/src/test/*.test.js",
|
|
15
|
+
"lint": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"llm",
|
|
19
|
+
"proxy",
|
|
20
|
+
"context",
|
|
21
|
+
"optimization",
|
|
22
|
+
"anthropic",
|
|
23
|
+
"openai",
|
|
24
|
+
"ollama"
|
|
25
|
+
],
|
|
26
|
+
"author": "Emil Vrána",
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@lancedb/lancedb": "^0.27.1"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ProviderConfig, SmartContextConfig } from './schema.js';
|
|
2
|
+
import { DEFAULT_CONFIG } from './defaults.js';
|
|
3
|
+
|
|
4
|
+
const PROVIDER_ENV_MAP: Record<string, { envKey: string; baseUrl: string }> = {
|
|
5
|
+
anthropic: {
|
|
6
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
7
|
+
baseUrl: 'https://api.anthropic.com',
|
|
8
|
+
},
|
|
9
|
+
openai: {
|
|
10
|
+
envKey: 'OPENAI_API_KEY',
|
|
11
|
+
baseUrl: 'https://api.openai.com',
|
|
12
|
+
},
|
|
13
|
+
google: {
|
|
14
|
+
envKey: 'GOOGLE_API_KEY',
|
|
15
|
+
baseUrl: 'https://generativelanguage.googleapis.com',
|
|
16
|
+
},
|
|
17
|
+
openrouter: {
|
|
18
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
19
|
+
baseUrl: 'https://openrouter.ai/api',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function detectProviders(): Record<string, ProviderConfig> {
|
|
24
|
+
const providers: Record<string, ProviderConfig> = {};
|
|
25
|
+
|
|
26
|
+
for (const [name, { envKey, baseUrl }] of Object.entries(PROVIDER_ENV_MAP)) {
|
|
27
|
+
const apiKey = process.env[envKey];
|
|
28
|
+
if (apiKey) {
|
|
29
|
+
providers[name] = { apiKey, baseUrl };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ollama: detect by host, no API key needed
|
|
34
|
+
const ollamaHost = process.env['OLLAMA_HOST'] || 'http://localhost:11434';
|
|
35
|
+
providers['ollama'] = { apiKey: '', baseUrl: ollamaHost };
|
|
36
|
+
|
|
37
|
+
return providers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildConfig(overrides?: Partial<SmartContextConfig>): SmartContextConfig {
|
|
41
|
+
const providers = detectProviders();
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...DEFAULT_CONFIG,
|
|
45
|
+
providers,
|
|
46
|
+
...overrides,
|
|
47
|
+
proxy: { ...DEFAULT_CONFIG.proxy, ...overrides?.proxy },
|
|
48
|
+
context: { ...DEFAULT_CONFIG.context, ...overrides?.context },
|
|
49
|
+
logging: { ...DEFAULT_CONFIG.logging, ...overrides?.logging },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SmartContextConfig } from './schema.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CONFIG: SmartContextConfig = {
|
|
4
|
+
proxy: {
|
|
5
|
+
port: 4800,
|
|
6
|
+
host: '127.0.0.1',
|
|
7
|
+
},
|
|
8
|
+
providers: {},
|
|
9
|
+
context: {
|
|
10
|
+
tier1_exchanges: 3,
|
|
11
|
+
tier2_max_chunks: 10,
|
|
12
|
+
tier2_min_score: 0.55,
|
|
13
|
+
tier3_token_reserve: 500,
|
|
14
|
+
recency_boost: 0.15,
|
|
15
|
+
filepath_boost: 0.20,
|
|
16
|
+
dedup_threshold: 0.92,
|
|
17
|
+
confidence_gate: 0.55,
|
|
18
|
+
response_reserve_tokens: 8192,
|
|
19
|
+
},
|
|
20
|
+
logging: {
|
|
21
|
+
level: 'info',
|
|
22
|
+
raw_logs: true,
|
|
23
|
+
metrics: true,
|
|
24
|
+
debug_headers: false,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SmartContextConfig {
|
|
2
|
+
proxy: {
|
|
3
|
+
port: number;
|
|
4
|
+
host: string;
|
|
5
|
+
};
|
|
6
|
+
providers: Record<string, ProviderConfig>;
|
|
7
|
+
context: ContextConfig;
|
|
8
|
+
logging: LoggingConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProviderConfig {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ContextConfig {
|
|
17
|
+
tier1_exchanges: number;
|
|
18
|
+
tier2_max_chunks: number;
|
|
19
|
+
tier2_min_score: number;
|
|
20
|
+
tier3_token_reserve: number;
|
|
21
|
+
recency_boost: number;
|
|
22
|
+
filepath_boost: number;
|
|
23
|
+
dedup_threshold: number;
|
|
24
|
+
confidence_gate: number;
|
|
25
|
+
response_reserve_tokens: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LoggingConfig {
|
|
29
|
+
level: 'error' | 'warn' | 'info' | 'debug';
|
|
30
|
+
raw_logs: boolean;
|
|
31
|
+
metrics: boolean;
|
|
32
|
+
debug_headers: boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ScoredChunk } from '../storage/types.js';
|
|
2
|
+
import type { CanonicalMessage } from './canonical.js';
|
|
3
|
+
import { getTextContent } from './canonical.js';
|
|
4
|
+
import { estimateTokens } from './chunker.js';
|
|
5
|
+
|
|
6
|
+
/** Known model context window sizes */
|
|
7
|
+
const MODEL_CONTEXT_LIMITS: Record<string, number> = {
|
|
8
|
+
'claude-opus-4-6': 200000,
|
|
9
|
+
'claude-sonnet-4-6': 200000,
|
|
10
|
+
'claude-haiku-4-5-20251001': 200000,
|
|
11
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
12
|
+
'gpt-4o': 128000,
|
|
13
|
+
'gpt-4o-mini': 128000,
|
|
14
|
+
'gpt-4-turbo': 128000,
|
|
15
|
+
'o1': 200000,
|
|
16
|
+
'o1-mini': 128000,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONTEXT_LIMIT = 128000;
|
|
20
|
+
|
|
21
|
+
export interface BudgetAllocation {
|
|
22
|
+
systemPromptTokens: number;
|
|
23
|
+
tier1Tokens: number;
|
|
24
|
+
tier2Budget: number;
|
|
25
|
+
tier3Reserve: number;
|
|
26
|
+
responseReserve: number;
|
|
27
|
+
totalAvailable: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PackedContext {
|
|
31
|
+
systemPrompt?: string;
|
|
32
|
+
tier1Messages: CanonicalMessage[];
|
|
33
|
+
tier2Chunks: ScoredChunk[];
|
|
34
|
+
tier3Summary?: string;
|
|
35
|
+
allocation: BudgetAllocation;
|
|
36
|
+
originalTokens: number;
|
|
37
|
+
optimizedTokens: number;
|
|
38
|
+
savingsPercent: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getModelContextLimit(model: string): number {
|
|
42
|
+
// Check exact match
|
|
43
|
+
if (MODEL_CONTEXT_LIMITS[model]) return MODEL_CONTEXT_LIMITS[model];
|
|
44
|
+
|
|
45
|
+
// Check prefix match
|
|
46
|
+
for (const [key, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
47
|
+
if (model.startsWith(key)) return limit;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return DEFAULT_CONTEXT_LIMIT;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Allocate token budget across tiers and pack context.
|
|
55
|
+
*/
|
|
56
|
+
export function packContext(
|
|
57
|
+
systemPrompt: string | undefined,
|
|
58
|
+
messages: CanonicalMessage[],
|
|
59
|
+
retrievedChunks: ScoredChunk[],
|
|
60
|
+
model: string,
|
|
61
|
+
tier1Exchanges: number,
|
|
62
|
+
tier3Reserve: number,
|
|
63
|
+
responseReserve: number,
|
|
64
|
+
): PackedContext {
|
|
65
|
+
const contextLimit = getModelContextLimit(model);
|
|
66
|
+
|
|
67
|
+
// Calculate original tokens
|
|
68
|
+
const originalTokens =
|
|
69
|
+
estimateTokens(systemPrompt || '') +
|
|
70
|
+
messages.reduce((sum, m) => sum + estimateTokens(getTextContent(m)), 0);
|
|
71
|
+
|
|
72
|
+
const systemPromptTokens = estimateTokens(systemPrompt || '');
|
|
73
|
+
|
|
74
|
+
// Extract Tier 1: last N exchanges (user+assistant pairs)
|
|
75
|
+
const tier1Messages: CanonicalMessage[] = [];
|
|
76
|
+
let exchangeCount = 0;
|
|
77
|
+
for (let i = messages.length - 1; i >= 0 && exchangeCount < tier1Exchanges; i--) {
|
|
78
|
+
tier1Messages.unshift(messages[i]);
|
|
79
|
+
if (messages[i].role === 'user') exchangeCount++;
|
|
80
|
+
}
|
|
81
|
+
const tier1Tokens = tier1Messages.reduce(
|
|
82
|
+
(sum, m) => sum + estimateTokens(getTextContent(m)),
|
|
83
|
+
0,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Calculate available budget for Tier 2
|
|
87
|
+
const totalAvailable = contextLimit - systemPromptTokens - responseReserve;
|
|
88
|
+
const tier2Budget = Math.max(0, totalAvailable - tier1Tokens - tier3Reserve);
|
|
89
|
+
|
|
90
|
+
// Pack Tier 2 chunks greedily by score
|
|
91
|
+
const tier2Chunks: ScoredChunk[] = [];
|
|
92
|
+
let tier2Used = 0;
|
|
93
|
+
for (const chunk of retrievedChunks) {
|
|
94
|
+
if (tier2Used + chunk.metadata.tokenCount <= tier2Budget) {
|
|
95
|
+
tier2Chunks.push(chunk);
|
|
96
|
+
tier2Used += chunk.metadata.tokenCount;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tier 3: summary placeholder (will be filled by summary system later)
|
|
101
|
+
const tier3Summary = undefined;
|
|
102
|
+
|
|
103
|
+
const optimizedTokens = systemPromptTokens + tier1Tokens + tier2Used;
|
|
104
|
+
|
|
105
|
+
const allocation: BudgetAllocation = {
|
|
106
|
+
systemPromptTokens,
|
|
107
|
+
tier1Tokens,
|
|
108
|
+
tier2Budget,
|
|
109
|
+
tier3Reserve,
|
|
110
|
+
responseReserve,
|
|
111
|
+
totalAvailable,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
systemPrompt,
|
|
116
|
+
tier1Messages,
|
|
117
|
+
tier2Chunks,
|
|
118
|
+
tier3Summary,
|
|
119
|
+
allocation,
|
|
120
|
+
originalTokens,
|
|
121
|
+
optimizedTokens,
|
|
122
|
+
savingsPercent: originalTokens > 0
|
|
123
|
+
? Math.round((1 - optimizedTokens / originalTokens) * 100)
|
|
124
|
+
: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|