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.
Files changed (166) hide show
  1. package/PLAN.md +406 -0
  2. package/PROGRESS.md +60 -0
  3. package/README.md +99 -0
  4. package/SPEC.md +915 -0
  5. package/adapters/openclaw/embedding.d.ts +8 -0
  6. package/adapters/openclaw/embedding.js +16 -0
  7. package/adapters/openclaw/embedding.ts +15 -0
  8. package/adapters/openclaw/index.d.ts +18 -0
  9. package/adapters/openclaw/index.js +42 -0
  10. package/adapters/openclaw/index.ts +43 -0
  11. package/adapters/openclaw/session-importer.d.ts +22 -0
  12. package/adapters/openclaw/session-importer.js +99 -0
  13. package/adapters/openclaw/session-importer.ts +105 -0
  14. package/adapters/openclaw/storage.d.ts +26 -0
  15. package/adapters/openclaw/storage.js +177 -0
  16. package/adapters/openclaw/storage.ts +183 -0
  17. package/dist/adapters/openclaw/embedding.d.ts +8 -0
  18. package/dist/adapters/openclaw/embedding.js +16 -0
  19. package/dist/adapters/openclaw/index.d.ts +18 -0
  20. package/dist/adapters/openclaw/index.js +42 -0
  21. package/dist/adapters/openclaw/session-importer.d.ts +22 -0
  22. package/dist/adapters/openclaw/session-importer.js +99 -0
  23. package/dist/adapters/openclaw/storage.d.ts +26 -0
  24. package/dist/adapters/openclaw/storage.js +177 -0
  25. package/dist/config/auto-detect.d.ts +3 -0
  26. package/dist/config/auto-detect.js +48 -0
  27. package/dist/config/defaults.d.ts +2 -0
  28. package/dist/config/defaults.js +28 -0
  29. package/dist/config/schema.d.ts +30 -0
  30. package/dist/config/schema.js +3 -0
  31. package/dist/context/budget.d.ts +25 -0
  32. package/dist/context/budget.js +85 -0
  33. package/dist/context/canonical.d.ts +39 -0
  34. package/dist/context/canonical.js +12 -0
  35. package/dist/context/chunker.d.ts +9 -0
  36. package/dist/context/chunker.js +148 -0
  37. package/dist/context/optimizer.d.ts +31 -0
  38. package/dist/context/optimizer.js +163 -0
  39. package/dist/context/retriever.d.ts +29 -0
  40. package/dist/context/retriever.js +103 -0
  41. package/dist/daemon/process.d.ts +6 -0
  42. package/dist/daemon/process.js +76 -0
  43. package/dist/daemon/service.d.ts +2 -0
  44. package/dist/daemon/service.js +99 -0
  45. package/dist/embedding/ollama.d.ts +11 -0
  46. package/dist/embedding/ollama.js +72 -0
  47. package/dist/embedding/types.d.ts +6 -0
  48. package/dist/embedding/types.js +3 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +190 -0
  51. package/dist/metrics/collector.d.ts +43 -0
  52. package/dist/metrics/collector.js +72 -0
  53. package/dist/providers/anthropic.d.ts +15 -0
  54. package/dist/providers/anthropic.js +109 -0
  55. package/dist/providers/google.d.ts +13 -0
  56. package/dist/providers/google.js +40 -0
  57. package/dist/providers/ollama.d.ts +13 -0
  58. package/dist/providers/ollama.js +82 -0
  59. package/dist/providers/openai.d.ts +15 -0
  60. package/dist/providers/openai.js +115 -0
  61. package/dist/providers/types.d.ts +18 -0
  62. package/dist/providers/types.js +3 -0
  63. package/dist/proxy/router.d.ts +12 -0
  64. package/dist/proxy/router.js +46 -0
  65. package/dist/proxy/server.d.ts +25 -0
  66. package/dist/proxy/server.js +265 -0
  67. package/dist/proxy/stream.d.ts +8 -0
  68. package/dist/proxy/stream.js +32 -0
  69. package/dist/src/config/auto-detect.d.ts +3 -0
  70. package/dist/src/config/auto-detect.js +48 -0
  71. package/dist/src/config/defaults.d.ts +2 -0
  72. package/dist/src/config/defaults.js +28 -0
  73. package/dist/src/config/schema.d.ts +30 -0
  74. package/dist/src/config/schema.js +3 -0
  75. package/dist/src/context/budget.d.ts +25 -0
  76. package/dist/src/context/budget.js +85 -0
  77. package/dist/src/context/canonical.d.ts +39 -0
  78. package/dist/src/context/canonical.js +12 -0
  79. package/dist/src/context/chunker.d.ts +9 -0
  80. package/dist/src/context/chunker.js +148 -0
  81. package/dist/src/context/optimizer.d.ts +31 -0
  82. package/dist/src/context/optimizer.js +163 -0
  83. package/dist/src/context/retriever.d.ts +29 -0
  84. package/dist/src/context/retriever.js +103 -0
  85. package/dist/src/daemon/process.d.ts +6 -0
  86. package/dist/src/daemon/process.js +76 -0
  87. package/dist/src/daemon/service.d.ts +2 -0
  88. package/dist/src/daemon/service.js +99 -0
  89. package/dist/src/embedding/ollama.d.ts +11 -0
  90. package/dist/src/embedding/ollama.js +72 -0
  91. package/dist/src/embedding/types.d.ts +6 -0
  92. package/dist/src/embedding/types.js +3 -0
  93. package/dist/src/index.d.ts +2 -0
  94. package/dist/src/index.js +190 -0
  95. package/dist/src/metrics/collector.d.ts +43 -0
  96. package/dist/src/metrics/collector.js +72 -0
  97. package/dist/src/providers/anthropic.d.ts +15 -0
  98. package/dist/src/providers/anthropic.js +109 -0
  99. package/dist/src/providers/google.d.ts +13 -0
  100. package/dist/src/providers/google.js +40 -0
  101. package/dist/src/providers/ollama.d.ts +13 -0
  102. package/dist/src/providers/ollama.js +82 -0
  103. package/dist/src/providers/openai.d.ts +15 -0
  104. package/dist/src/providers/openai.js +115 -0
  105. package/dist/src/providers/types.d.ts +18 -0
  106. package/dist/src/providers/types.js +3 -0
  107. package/dist/src/proxy/router.d.ts +12 -0
  108. package/dist/src/proxy/router.js +46 -0
  109. package/dist/src/proxy/server.d.ts +25 -0
  110. package/dist/src/proxy/server.js +265 -0
  111. package/dist/src/proxy/stream.d.ts +8 -0
  112. package/dist/src/proxy/stream.js +32 -0
  113. package/dist/src/storage/lancedb.d.ts +21 -0
  114. package/dist/src/storage/lancedb.js +158 -0
  115. package/dist/src/storage/types.d.ts +52 -0
  116. package/dist/src/storage/types.js +3 -0
  117. package/dist/src/test/context.test.d.ts +1 -0
  118. package/dist/src/test/context.test.js +141 -0
  119. package/dist/src/test/dashboard.test.d.ts +1 -0
  120. package/dist/src/test/dashboard.test.js +85 -0
  121. package/dist/src/test/proxy.test.d.ts +1 -0
  122. package/dist/src/test/proxy.test.js +188 -0
  123. package/dist/src/ui/dashboard.d.ts +2 -0
  124. package/dist/src/ui/dashboard.js +183 -0
  125. package/dist/storage/lancedb.d.ts +21 -0
  126. package/dist/storage/lancedb.js +158 -0
  127. package/dist/storage/types.d.ts +52 -0
  128. package/dist/storage/types.js +3 -0
  129. package/dist/test/context.test.d.ts +1 -0
  130. package/dist/test/context.test.js +141 -0
  131. package/dist/test/dashboard.test.d.ts +1 -0
  132. package/dist/test/dashboard.test.js +85 -0
  133. package/dist/test/proxy.test.d.ts +1 -0
  134. package/dist/test/proxy.test.js +188 -0
  135. package/dist/ui/dashboard.d.ts +2 -0
  136. package/dist/ui/dashboard.js +183 -0
  137. package/package.json +38 -0
  138. package/src/config/auto-detect.ts +51 -0
  139. package/src/config/defaults.ts +26 -0
  140. package/src/config/schema.ts +33 -0
  141. package/src/context/budget.ts +126 -0
  142. package/src/context/canonical.ts +50 -0
  143. package/src/context/chunker.ts +165 -0
  144. package/src/context/optimizer.ts +201 -0
  145. package/src/context/retriever.ts +123 -0
  146. package/src/daemon/process.ts +70 -0
  147. package/src/daemon/service.ts +103 -0
  148. package/src/embedding/ollama.ts +68 -0
  149. package/src/embedding/types.ts +6 -0
  150. package/src/index.ts +176 -0
  151. package/src/metrics/collector.ts +114 -0
  152. package/src/providers/anthropic.ts +117 -0
  153. package/src/providers/google.ts +42 -0
  154. package/src/providers/ollama.ts +87 -0
  155. package/src/providers/openai.ts +127 -0
  156. package/src/providers/types.ts +20 -0
  157. package/src/proxy/router.ts +48 -0
  158. package/src/proxy/server.ts +315 -0
  159. package/src/proxy/stream.ts +39 -0
  160. package/src/storage/lancedb.ts +169 -0
  161. package/src/storage/types.ts +47 -0
  162. package/src/test/context.test.ts +165 -0
  163. package/src/test/dashboard.test.ts +94 -0
  164. package/src/test/proxy.test.ts +218 -0
  165. package/src/ui/dashboard.ts +184 -0
  166. package/tsconfig.json +18 -0
@@ -0,0 +1,72 @@
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.OllamaEmbeddingAdapter = void 0;
7
+ const node_http_1 = __importDefault(require("node:http"));
8
+ const node_https_1 = __importDefault(require("node:https"));
9
+ const node_url_1 = require("node:url");
10
+ class OllamaEmbeddingAdapter {
11
+ url;
12
+ model;
13
+ name = 'ollama';
14
+ dimensions = 768; // nomic-embed-text default
15
+ constructor(url = 'http://localhost:11434', model = 'nomic-embed-text') {
16
+ this.url = url;
17
+ this.model = model;
18
+ }
19
+ async initialize() {
20
+ // Verify Ollama is reachable and model exists
21
+ try {
22
+ await this.embed(['test']);
23
+ }
24
+ catch (err) {
25
+ throw new Error(`Ollama embedding unavailable at ${this.url}: ${err}`);
26
+ }
27
+ }
28
+ async embed(texts) {
29
+ const results = [];
30
+ for (const text of texts) {
31
+ const embedding = await this.embedSingle(text);
32
+ results.push(embedding);
33
+ if (embedding.length !== this.dimensions) {
34
+ this.dimensions = embedding.length; // auto-detect dimensions
35
+ }
36
+ }
37
+ return results;
38
+ }
39
+ async embedSingle(text) {
40
+ const parsed = new node_url_1.URL(`${this.url}/api/embed`);
41
+ const transport = parsed.protocol === 'https:' ? node_https_1.default : node_http_1.default;
42
+ const body = JSON.stringify({ model: this.model, input: text });
43
+ return new Promise((resolve, reject) => {
44
+ const req = transport.request(parsed, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
45
+ let data = '';
46
+ res.on('data', (chunk) => (data += chunk));
47
+ res.on('end', () => {
48
+ try {
49
+ const parsed = JSON.parse(data);
50
+ if (parsed.embeddings?.[0]) {
51
+ resolve(parsed.embeddings[0]);
52
+ }
53
+ else if (parsed.embedding) {
54
+ resolve(parsed.embedding);
55
+ }
56
+ else {
57
+ reject(new Error(`Unexpected Ollama response: ${data.slice(0, 200)}`));
58
+ }
59
+ }
60
+ catch (e) {
61
+ reject(new Error(`Failed to parse Ollama response: ${e}`));
62
+ }
63
+ });
64
+ });
65
+ req.on('error', reject);
66
+ req.write(body);
67
+ req.end();
68
+ });
69
+ }
70
+ }
71
+ exports.OllamaEmbeddingAdapter = OllamaEmbeddingAdapter;
72
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1,6 @@
1
+ export interface EmbeddingAdapter {
2
+ name: string;
3
+ dimensions: number;
4
+ embed(texts: string[]): Promise<number[][]>;
5
+ initialize(): Promise<void>;
6
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const auto_detect_js_1 = require("./config/auto-detect.js");
8
+ const server_js_1 = require("./proxy/server.js");
9
+ const ollama_js_1 = require("./embedding/ollama.js");
10
+ const lancedb_js_1 = require("./storage/lancedb.js");
11
+ const process_js_1 = require("./daemon/process.js");
12
+ const service_js_1 = require("./daemon/service.js");
13
+ const node_http_1 = __importDefault(require("node:http"));
14
+ const VERSION = '0.1.0';
15
+ function parseArgs(args) {
16
+ const result = {};
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ if (arg === '--port' || arg === '-p')
20
+ result.port = args[++i];
21
+ else if (arg === '--config' || arg === '-c')
22
+ result.config = args[++i];
23
+ else if (arg === '--help' || arg === '-h')
24
+ result.help = true;
25
+ else if (arg === '--version' || arg === '-v')
26
+ result.version = true;
27
+ else if (arg === '--no-optimize')
28
+ result.noOptimize = true;
29
+ else if (arg === '--embedding-url')
30
+ result.embeddingUrl = args[++i];
31
+ else if (arg === '--embedding-model')
32
+ result.embeddingModel = args[++i];
33
+ else if (arg === '--data-dir')
34
+ result.dataDir = args[++i];
35
+ else if (!arg.startsWith('-'))
36
+ result.command = arg;
37
+ }
38
+ return result;
39
+ }
40
+ function printHelp() {
41
+ console.log(`
42
+ SmartContext Proxy v${VERSION}
43
+ Intelligent context window optimization for LLM APIs
44
+
45
+ Usage:
46
+ smartcontext-proxy [options]
47
+ smartcontext-proxy status Show proxy status
48
+
49
+ Options:
50
+ --port, -p <port> Proxy port (default: 4800)
51
+ --config, -c <file> Config file path
52
+ --no-optimize Run in transparent proxy mode (no context optimization)
53
+ --embedding-url <url> Ollama URL for embeddings (default: http://localhost:11434)
54
+ --embedding-model <model> Embedding model (default: nomic-embed-text)
55
+ --data-dir <path> Data directory (default: ~/.smartcontext/data)
56
+ --help, -h Show help
57
+ --version, -v Show version
58
+
59
+ Client Integration:
60
+ ANTHROPIC_API_URL=http://localhost:4800/v1/anthropic
61
+ OPENAI_BASE_URL=http://localhost:4800/v1/openai
62
+ OLLAMA_HOST=http://localhost:4800/v1/ollama
63
+
64
+ API:
65
+ GET /health Health check
66
+ GET /_sc/status Proxy status
67
+ GET /_sc/stats Aggregate metrics
68
+ GET /_sc/feed Recent requests
69
+ POST /_sc/pause Pause optimization
70
+ POST /_sc/resume Resume optimization
71
+ `);
72
+ }
73
+ async function showStatus(port) {
74
+ return new Promise((resolve) => {
75
+ node_http_1.default.get(`http://127.0.0.1:${port}/_sc/status`, (res) => {
76
+ let data = '';
77
+ res.on('data', (chunk) => (data += chunk));
78
+ res.on('end', () => {
79
+ try {
80
+ const status = JSON.parse(data);
81
+ console.log(`SmartContext Proxy: ${status.state}`);
82
+ console.log(` Uptime: ${Math.round(status.uptime / 1000)}s`);
83
+ console.log(` Requests: ${status.requests}`);
84
+ console.log(` Mode: ${status.mode}`);
85
+ }
86
+ catch {
87
+ console.log('Could not parse status response');
88
+ }
89
+ resolve();
90
+ });
91
+ }).on('error', () => {
92
+ console.log(`SmartContext Proxy: not running on port ${port}`);
93
+ resolve();
94
+ });
95
+ });
96
+ }
97
+ async function main() {
98
+ const args = parseArgs(process.argv.slice(2));
99
+ if (args.version) {
100
+ console.log(VERSION);
101
+ return;
102
+ }
103
+ if (args.help) {
104
+ printHelp();
105
+ return;
106
+ }
107
+ const port = args.port ? parseInt(args.port, 10) : 4800;
108
+ if (args.command === 'status') {
109
+ await showStatus(port);
110
+ return;
111
+ }
112
+ if (args.command === 'stop') {
113
+ (0, process_js_1.stopDaemon)();
114
+ return;
115
+ }
116
+ if (args.command === 'start') {
117
+ (0, process_js_1.startDaemon)(process.argv.slice(3));
118
+ return;
119
+ }
120
+ if (args.command === 'restart') {
121
+ (0, process_js_1.stopDaemon)();
122
+ await new Promise(r => setTimeout(r, 1000));
123
+ (0, process_js_1.startDaemon)(process.argv.slice(3));
124
+ return;
125
+ }
126
+ if (args.command === 'install-service') {
127
+ const path = (0, service_js_1.installService)(port);
128
+ console.log(`Service installed: ${path}`);
129
+ return;
130
+ }
131
+ if (args.command === 'uninstall-service') {
132
+ console.log((0, service_js_1.uninstallService)());
133
+ return;
134
+ }
135
+ const config = (0, auto_detect_js_1.buildConfig)({
136
+ proxy: { port, host: '127.0.0.1' },
137
+ });
138
+ // Initialize embedding and storage (unless --no-optimize)
139
+ let embedding;
140
+ let storage;
141
+ if (!args.noOptimize) {
142
+ try {
143
+ const embeddingUrl = args.embeddingUrl || process.env['OLLAMA_HOST'] || 'http://localhost:11434';
144
+ const embeddingModel = args.embeddingModel || 'nomic-embed-text';
145
+ const dataDir = args.dataDir;
146
+ embedding = new ollama_js_1.OllamaEmbeddingAdapter(embeddingUrl, embeddingModel);
147
+ await embedding.initialize();
148
+ storage = new lancedb_js_1.LanceDBAdapter(dataDir);
149
+ await storage.initialize();
150
+ console.log(` Embedding: ${embeddingModel} @ ${embeddingUrl}`);
151
+ console.log(` Storage: LanceDB`);
152
+ }
153
+ catch (err) {
154
+ console.log(` Optimization unavailable: ${err}`);
155
+ console.log(` Running in transparent proxy mode`);
156
+ embedding = undefined;
157
+ storage = undefined;
158
+ }
159
+ }
160
+ const server = new server_js_1.ProxyServer(config, embedding, storage);
161
+ const providers = server.getProviderNames();
162
+ const mode = embedding && storage ? 'optimizing' : 'transparent';
163
+ await server.start();
164
+ console.log(`
165
+ ┌─────────────────────────────────────────────┐
166
+ │ SmartContext Proxy v${VERSION} │
167
+ │ http://${config.proxy.host}:${config.proxy.port} │
168
+ │ │
169
+ │ Providers: ${providers.join(', ').padEnd(31)}│
170
+ │ Mode: ${mode.padEnd(36)}│
171
+ └─────────────────────────────────────────────┘
172
+ `);
173
+ // Write PID file
174
+ (0, process_js_1.writePid)();
175
+ const shutdown = async () => {
176
+ console.log('\nShutting down...');
177
+ (0, process_js_1.removePid)();
178
+ await server.stop();
179
+ if (storage)
180
+ await storage.close();
181
+ process.exit(0);
182
+ };
183
+ process.on('SIGINT', shutdown);
184
+ process.on('SIGTERM', shutdown);
185
+ }
186
+ main().catch((err) => {
187
+ console.error('Fatal:', err);
188
+ process.exit(1);
189
+ });
190
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,43 @@
1
+ export interface RequestMetric {
2
+ id: number;
3
+ timestamp: number;
4
+ provider: string;
5
+ model: string;
6
+ streaming: boolean;
7
+ originalTokens: number;
8
+ optimizedTokens: number;
9
+ savingsPercent: number;
10
+ latencyOverheadMs: number;
11
+ chunksRetrieved: number;
12
+ topScore: number;
13
+ passThrough: boolean;
14
+ reason?: string;
15
+ }
16
+ export interface AggregateStats {
17
+ totalRequests: number;
18
+ totalOriginalTokens: number;
19
+ totalOptimizedTokens: number;
20
+ totalSavingsPercent: number;
21
+ avgLatencyOverheadMs: number;
22
+ avgChunksRetrieved: number;
23
+ byProvider: Record<string, ProviderStats>;
24
+ byModel: Record<string, ModelStats>;
25
+ }
26
+ export interface ProviderStats {
27
+ requests: number;
28
+ tokensSaved: number;
29
+ savingsPercent: number;
30
+ }
31
+ export interface ModelStats {
32
+ requests: number;
33
+ tokensSaved: number;
34
+ savingsPercent: number;
35
+ }
36
+ export declare class MetricsCollector {
37
+ private metrics;
38
+ private startTime;
39
+ record(metric: RequestMetric): void;
40
+ getRecent(limit?: number): RequestMetric[];
41
+ getStats(): AggregateStats;
42
+ getUptime(): number;
43
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsCollector = void 0;
4
+ class MetricsCollector {
5
+ metrics = [];
6
+ startTime = Date.now();
7
+ record(metric) {
8
+ this.metrics.push(metric);
9
+ }
10
+ getRecent(limit = 50) {
11
+ return this.metrics.slice(-limit);
12
+ }
13
+ getStats() {
14
+ const total = this.metrics.length;
15
+ if (total === 0) {
16
+ return {
17
+ totalRequests: 0,
18
+ totalOriginalTokens: 0,
19
+ totalOptimizedTokens: 0,
20
+ totalSavingsPercent: 0,
21
+ avgLatencyOverheadMs: 0,
22
+ avgChunksRetrieved: 0,
23
+ byProvider: {},
24
+ byModel: {},
25
+ };
26
+ }
27
+ let totalOriginal = 0;
28
+ let totalOptimized = 0;
29
+ let totalLatency = 0;
30
+ let totalChunks = 0;
31
+ const byProvider = {};
32
+ const byModel = {};
33
+ for (const m of this.metrics) {
34
+ totalOriginal += m.originalTokens;
35
+ totalOptimized += m.optimizedTokens;
36
+ totalLatency += m.latencyOverheadMs;
37
+ totalChunks += m.chunksRetrieved;
38
+ // By provider
39
+ if (!byProvider[m.provider])
40
+ byProvider[m.provider] = { requests: 0, tokensSaved: 0, savingsPercent: 0 };
41
+ byProvider[m.provider].requests++;
42
+ byProvider[m.provider].tokensSaved += m.originalTokens - m.optimizedTokens;
43
+ // By model
44
+ if (!byModel[m.model])
45
+ byModel[m.model] = { requests: 0, tokensSaved: 0, savingsPercent: 0 };
46
+ byModel[m.model].requests++;
47
+ byModel[m.model].tokensSaved += m.originalTokens - m.optimizedTokens;
48
+ }
49
+ // Calculate percentages
50
+ for (const stats of Object.values(byProvider)) {
51
+ stats.savingsPercent = totalOriginal > 0 ? Math.round((stats.tokensSaved / totalOriginal) * 100) : 0;
52
+ }
53
+ for (const stats of Object.values(byModel)) {
54
+ stats.savingsPercent = totalOriginal > 0 ? Math.round((stats.tokensSaved / totalOriginal) * 100) : 0;
55
+ }
56
+ return {
57
+ totalRequests: total,
58
+ totalOriginalTokens: totalOriginal,
59
+ totalOptimizedTokens: totalOptimized,
60
+ totalSavingsPercent: totalOriginal > 0 ? Math.round((1 - totalOptimized / totalOriginal) * 100) : 0,
61
+ avgLatencyOverheadMs: Math.round(totalLatency / total),
62
+ avgChunksRetrieved: Math.round(totalChunks / total * 10) / 10,
63
+ byProvider,
64
+ byModel,
65
+ };
66
+ }
67
+ getUptime() {
68
+ return Date.now() - this.startTime;
69
+ }
70
+ }
71
+ exports.MetricsCollector = MetricsCollector;
72
+ //# sourceMappingURL=collector.js.map
@@ -0,0 +1,15 @@
1
+ import type { ProviderAdapter } from './types.js';
2
+ import type { CanonicalRequest } from '../context/canonical.js';
3
+ export declare class AnthropicAdapter implements ProviderAdapter {
4
+ baseUrl: string;
5
+ name: string;
6
+ constructor(baseUrl?: string);
7
+ parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
8
+ serializeRequest(canonical: CanonicalRequest): unknown;
9
+ forwardUrl(originalPath: string): string;
10
+ extractApiKey(headers: Record<string, string>): string;
11
+ contentType(): string;
12
+ authHeaders(apiKey: string): Record<string, string>;
13
+ private parseMessage;
14
+ private serializeMessage;
15
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnthropicAdapter = void 0;
4
+ class AnthropicAdapter {
5
+ baseUrl;
6
+ name = 'anthropic';
7
+ constructor(baseUrl = 'https://api.anthropic.com') {
8
+ this.baseUrl = baseUrl;
9
+ }
10
+ parseRequest(body, headers) {
11
+ const b = body;
12
+ const messages = [];
13
+ let systemPrompt;
14
+ // Extract system prompt
15
+ if (typeof b.system === 'string') {
16
+ systemPrompt = b.system;
17
+ }
18
+ else if (Array.isArray(b.system)) {
19
+ systemPrompt = b.system
20
+ .map((s) => s.text || '')
21
+ .join('\n');
22
+ }
23
+ // Convert messages
24
+ if (Array.isArray(b.messages)) {
25
+ for (const msg of b.messages) {
26
+ messages.push(this.parseMessage(msg));
27
+ }
28
+ }
29
+ return {
30
+ messages,
31
+ systemPrompt,
32
+ model: b.model || 'unknown',
33
+ stream: !!b.stream,
34
+ maxTokens: b.max_tokens,
35
+ temperature: b.temperature,
36
+ tools: b.tools,
37
+ rawHeaders: headers,
38
+ providerAuth: this.extractApiKey(headers),
39
+ };
40
+ }
41
+ serializeRequest(canonical) {
42
+ const body = {
43
+ model: canonical.model,
44
+ messages: canonical.messages.map((m) => this.serializeMessage(m)),
45
+ stream: canonical.stream,
46
+ };
47
+ if (canonical.systemPrompt) {
48
+ body.system = canonical.systemPrompt;
49
+ }
50
+ if (canonical.maxTokens) {
51
+ body.max_tokens = canonical.maxTokens;
52
+ }
53
+ if (canonical.temperature !== undefined) {
54
+ body.temperature = canonical.temperature;
55
+ }
56
+ if (canonical.tools) {
57
+ body.tools = canonical.tools;
58
+ }
59
+ return body;
60
+ }
61
+ forwardUrl(originalPath) {
62
+ // /v1/anthropic/v1/messages → https://api.anthropic.com/v1/messages
63
+ const stripped = originalPath.replace(/^\/v1\/anthropic/, '');
64
+ return `${this.baseUrl}${stripped}`;
65
+ }
66
+ extractApiKey(headers) {
67
+ return headers['x-api-key'] || '';
68
+ }
69
+ contentType() {
70
+ return 'application/json';
71
+ }
72
+ authHeaders(apiKey) {
73
+ return {
74
+ 'x-api-key': apiKey,
75
+ 'anthropic-version': '2023-06-01',
76
+ };
77
+ }
78
+ parseMessage(msg) {
79
+ const role = msg.role;
80
+ let content;
81
+ if (typeof msg.content === 'string') {
82
+ content = msg.content;
83
+ }
84
+ else if (Array.isArray(msg.content)) {
85
+ content = msg.content.map((block) => {
86
+ if (block.type === 'text') {
87
+ return { type: 'text', text: block.text };
88
+ }
89
+ // Pass through tool_use, tool_result, image blocks as-is
90
+ return { ...block, type: block.type };
91
+ });
92
+ }
93
+ else {
94
+ content = '';
95
+ }
96
+ return {
97
+ role: role,
98
+ content,
99
+ };
100
+ }
101
+ serializeMessage(msg) {
102
+ return {
103
+ role: msg.role,
104
+ content: msg.content,
105
+ };
106
+ }
107
+ }
108
+ exports.AnthropicAdapter = AnthropicAdapter;
109
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1,13 @@
1
+ import type { ProviderAdapter } from './types.js';
2
+ import type { CanonicalRequest } from '../context/canonical.js';
3
+ export declare class GoogleAdapter implements ProviderAdapter {
4
+ baseUrl: string;
5
+ name: string;
6
+ constructor(baseUrl?: string);
7
+ parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
8
+ serializeRequest(canonical: CanonicalRequest): unknown;
9
+ forwardUrl(originalPath: string): string;
10
+ extractApiKey(headers: Record<string, string>): string;
11
+ contentType(): string;
12
+ authHeaders(apiKey: string): Record<string, string>;
13
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GoogleAdapter = void 0;
4
+ class GoogleAdapter {
5
+ baseUrl;
6
+ name = 'google';
7
+ constructor(baseUrl = 'https://generativelanguage.googleapis.com') {
8
+ this.baseUrl = baseUrl;
9
+ }
10
+ parseRequest(body, headers) {
11
+ // Stub: pass through as-is for Phase 1
12
+ const b = body;
13
+ return {
14
+ messages: [],
15
+ model: 'unknown',
16
+ stream: !!b.stream,
17
+ rawHeaders: headers,
18
+ providerAuth: this.extractApiKey(headers),
19
+ };
20
+ }
21
+ serializeRequest(canonical) {
22
+ // Stub: not implemented for Phase 1
23
+ return {};
24
+ }
25
+ forwardUrl(originalPath) {
26
+ const stripped = originalPath.replace(/^\/v1\/google/, '');
27
+ return `${this.baseUrl}${stripped}`;
28
+ }
29
+ extractApiKey(headers) {
30
+ return headers['x-goog-api-key'] || '';
31
+ }
32
+ contentType() {
33
+ return 'application/json';
34
+ }
35
+ authHeaders(apiKey) {
36
+ return { 'x-goog-api-key': apiKey };
37
+ }
38
+ }
39
+ exports.GoogleAdapter = GoogleAdapter;
40
+ //# sourceMappingURL=google.js.map
@@ -0,0 +1,13 @@
1
+ import type { ProviderAdapter } from './types.js';
2
+ import type { CanonicalRequest } from '../context/canonical.js';
3
+ export declare class OllamaAdapter implements ProviderAdapter {
4
+ baseUrl: string;
5
+ name: string;
6
+ constructor(baseUrl?: string);
7
+ parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
8
+ serializeRequest(canonical: CanonicalRequest): unknown;
9
+ forwardUrl(originalPath: string): string;
10
+ extractApiKey(headers: Record<string, string>): string;
11
+ contentType(): string;
12
+ authHeaders(apiKey: string): Record<string, string>;
13
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OllamaAdapter = void 0;
4
+ class OllamaAdapter {
5
+ baseUrl;
6
+ name = 'ollama';
7
+ constructor(baseUrl = 'http://localhost:11434') {
8
+ this.baseUrl = baseUrl;
9
+ }
10
+ parseRequest(body, headers) {
11
+ const b = body;
12
+ const messages = [];
13
+ let systemPrompt;
14
+ if (Array.isArray(b.messages)) {
15
+ for (const msg of b.messages) {
16
+ if (msg.role === 'system') {
17
+ systemPrompt = msg.content;
18
+ continue;
19
+ }
20
+ messages.push({
21
+ role: msg.role,
22
+ content: msg.content || '',
23
+ });
24
+ }
25
+ }
26
+ return {
27
+ messages,
28
+ systemPrompt,
29
+ model: b.model || 'unknown',
30
+ stream: b.stream !== false, // Ollama streams by default
31
+ maxTokens: b.options ? b.options.num_predict : undefined,
32
+ temperature: b.options ? b.options.temperature : undefined,
33
+ rawHeaders: headers,
34
+ providerAuth: '',
35
+ };
36
+ }
37
+ serializeRequest(canonical) {
38
+ const messages = [];
39
+ if (canonical.systemPrompt) {
40
+ messages.push({ role: 'system', content: canonical.systemPrompt });
41
+ }
42
+ for (const msg of canonical.messages) {
43
+ messages.push({
44
+ role: msg.role,
45
+ content: typeof msg.content === 'string'
46
+ ? msg.content
47
+ : msg.content.filter((b) => b.type === 'text').map((b) => b.text || '').join('\n'),
48
+ });
49
+ }
50
+ const body = {
51
+ model: canonical.model,
52
+ messages,
53
+ stream: canonical.stream,
54
+ };
55
+ const options = {};
56
+ if (canonical.maxTokens)
57
+ options.num_predict = canonical.maxTokens;
58
+ if (canonical.temperature !== undefined)
59
+ options.temperature = canonical.temperature;
60
+ if (Object.keys(options).length > 0)
61
+ body.options = options;
62
+ return body;
63
+ }
64
+ forwardUrl(originalPath) {
65
+ // /v1/ollama/api/chat → http://localhost:11434/api/chat
66
+ const stripped = originalPath.replace(/^\/v1\/ollama/, '');
67
+ return `${this.baseUrl}${stripped}`;
68
+ }
69
+ extractApiKey(headers) {
70
+ return headers['authorization']?.replace(/^Bearer\s+/i, '') || '';
71
+ }
72
+ contentType() {
73
+ return 'application/json';
74
+ }
75
+ authHeaders(apiKey) {
76
+ if (!apiKey)
77
+ return {};
78
+ return { 'Authorization': `Bearer ${apiKey}` };
79
+ }
80
+ }
81
+ exports.OllamaAdapter = OllamaAdapter;
82
+ //# sourceMappingURL=ollama.js.map