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,127 @@
1
+ import type { ProviderAdapter } from './types.js';
2
+ import type { CanonicalMessage, CanonicalRequest, ContentBlock } from '../context/canonical.js';
3
+
4
+ export class OpenAIAdapter implements ProviderAdapter {
5
+ name = 'openai';
6
+
7
+ constructor(public baseUrl: string = 'https://api.openai.com') {}
8
+
9
+ parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest {
10
+ const b = body as Record<string, unknown>;
11
+ const messages: CanonicalMessage[] = [];
12
+ let systemPrompt: string | undefined;
13
+
14
+ if (Array.isArray(b.messages)) {
15
+ for (const msg of b.messages as Array<Record<string, unknown>>) {
16
+ // OpenAI puts system prompt as a message with role=system
17
+ if (msg.role === 'system') {
18
+ systemPrompt = typeof msg.content === 'string'
19
+ ? msg.content
20
+ : (msg.content as Array<{ text?: string }>)?.map((c) => c.text || '').join('\n');
21
+ continue;
22
+ }
23
+ messages.push(this.parseMessage(msg));
24
+ }
25
+ }
26
+
27
+ return {
28
+ messages,
29
+ systemPrompt,
30
+ model: (b.model as string) || 'unknown',
31
+ stream: !!b.stream,
32
+ maxTokens: (b.max_tokens ?? b.max_completion_tokens) as number | undefined,
33
+ temperature: b.temperature as number | undefined,
34
+ tools: b.tools as unknown[] | undefined,
35
+ rawHeaders: headers,
36
+ providerAuth: this.extractApiKey(headers),
37
+ };
38
+ }
39
+
40
+ serializeRequest(canonical: CanonicalRequest): unknown {
41
+ const messages: Array<Record<string, unknown>> = [];
42
+
43
+ if (canonical.systemPrompt) {
44
+ messages.push({ role: 'system', content: canonical.systemPrompt });
45
+ }
46
+
47
+ for (const msg of canonical.messages) {
48
+ messages.push(this.serializeMessage(msg));
49
+ }
50
+
51
+ const body: Record<string, unknown> = {
52
+ model: canonical.model,
53
+ messages,
54
+ stream: canonical.stream,
55
+ };
56
+
57
+ if (canonical.maxTokens) {
58
+ body.max_tokens = canonical.maxTokens;
59
+ }
60
+ if (canonical.temperature !== undefined) {
61
+ body.temperature = canonical.temperature;
62
+ }
63
+ if (canonical.tools) {
64
+ body.tools = canonical.tools;
65
+ }
66
+
67
+ return body;
68
+ }
69
+
70
+ forwardUrl(originalPath: string): string {
71
+ // /v1/openai/v1/chat/completions → https://api.openai.com/v1/chat/completions
72
+ const stripped = originalPath.replace(/^\/v1\/openai/, '');
73
+ return `${this.baseUrl}${stripped}`;
74
+ }
75
+
76
+ extractApiKey(headers: Record<string, string>): string {
77
+ const auth = headers['authorization'] || '';
78
+ return auth.replace(/^Bearer\s+/i, '');
79
+ }
80
+
81
+ contentType(): string {
82
+ return 'application/json';
83
+ }
84
+
85
+ authHeaders(apiKey: string): Record<string, string> {
86
+ return {
87
+ 'Authorization': `Bearer ${apiKey}`,
88
+ };
89
+ }
90
+
91
+ private parseMessage(msg: Record<string, unknown>): CanonicalMessage {
92
+ const role = msg.role as string;
93
+ let content: string | ContentBlock[];
94
+
95
+ if (typeof msg.content === 'string') {
96
+ content = msg.content;
97
+ } else if (Array.isArray(msg.content)) {
98
+ content = (msg.content as Array<Record<string, unknown>>).map((block) => {
99
+ if (block.type === 'text') {
100
+ return { type: 'text' as const, text: block.text as string };
101
+ }
102
+ return { ...block, type: block.type as ContentBlock['type'] };
103
+ });
104
+ } else if (msg.content === null || msg.content === undefined) {
105
+ // Assistant messages with tool_calls may have null content
106
+ content = '';
107
+ } else {
108
+ content = '';
109
+ }
110
+
111
+ const canonical: CanonicalMessage = { role: role as CanonicalMessage['role'], content };
112
+
113
+ // Preserve tool_calls on assistant messages
114
+ if (msg.tool_calls) {
115
+ canonical.metadata = { tools: (msg.tool_calls as Array<{ function?: { name?: string } }>).map((t) => t.function?.name || 'unknown') };
116
+ }
117
+
118
+ return canonical;
119
+ }
120
+
121
+ private serializeMessage(msg: CanonicalMessage): Record<string, unknown> {
122
+ return {
123
+ role: msg.role === 'tool' ? 'tool' : msg.role,
124
+ content: msg.content,
125
+ };
126
+ }
127
+ }
@@ -0,0 +1,20 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { CanonicalRequest, CanonicalResponse } from '../context/canonical.js';
3
+
4
+ export interface ProviderAdapter {
5
+ name: string;
6
+ /** Base URL for this provider's API */
7
+ baseUrl: string;
8
+ /** Parse raw request body into canonical format */
9
+ parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
10
+ /** Convert canonical request back to provider format for forwarding */
11
+ serializeRequest(canonical: CanonicalRequest): unknown;
12
+ /** Build the forward URL from the original request path */
13
+ forwardUrl(originalPath: string): string;
14
+ /** Extract API key from request headers */
15
+ extractApiKey(headers: Record<string, string>): string;
16
+ /** Get the content-type header for forwarding */
17
+ contentType(): string;
18
+ /** Build auth headers for the forwarded request */
19
+ authHeaders(apiKey: string): Record<string, string>;
20
+ }
@@ -0,0 +1,48 @@
1
+ import type { ProviderAdapter } from '../providers/types.js';
2
+ import { AnthropicAdapter } from '../providers/anthropic.js';
3
+ import { OpenAIAdapter } from '../providers/openai.js';
4
+ import { OllamaAdapter } from '../providers/ollama.js';
5
+ import { GoogleAdapter } from '../providers/google.js';
6
+ import type { SmartContextConfig } from '../config/schema.js';
7
+
8
+ export class Router {
9
+ private adapters: Map<string, ProviderAdapter> = new Map();
10
+
11
+ constructor(config: SmartContextConfig) {
12
+ // Register adapters for detected providers
13
+ const providers = config.providers;
14
+
15
+ if (providers.anthropic) {
16
+ this.adapters.set('anthropic', new AnthropicAdapter(providers.anthropic.baseUrl));
17
+ }
18
+ if (providers.openai) {
19
+ this.adapters.set('openai', new OpenAIAdapter(providers.openai.baseUrl));
20
+ }
21
+ if (providers.ollama) {
22
+ this.adapters.set('ollama', new OllamaAdapter(providers.ollama.baseUrl));
23
+ }
24
+ if (providers.google) {
25
+ this.adapters.set('google', new GoogleAdapter(providers.google.baseUrl));
26
+ }
27
+ if (providers.openrouter) {
28
+ // OpenRouter uses OpenAI-compatible API
29
+ this.adapters.set('openrouter', new OpenAIAdapter(providers.openrouter.baseUrl || 'https://openrouter.ai/api'));
30
+ }
31
+ }
32
+
33
+ /** Extract provider name from URL path: /v1/{provider}/... */
34
+ resolve(path: string): { adapter: ProviderAdapter; providerName: string } | null {
35
+ const match = path.match(/^\/v1\/([^/]+)/);
36
+ if (!match) return null;
37
+
38
+ const providerName = match[1];
39
+ const adapter = this.adapters.get(providerName);
40
+ if (!adapter) return null;
41
+
42
+ return { adapter, providerName };
43
+ }
44
+
45
+ getProviderNames(): string[] {
46
+ return Array.from(this.adapters.keys());
47
+ }
48
+ }
@@ -0,0 +1,315 @@
1
+ import http, { IncomingMessage, ServerResponse } from 'node:http';
2
+ import https from 'node:https';
3
+ import { URL } from 'node:url';
4
+ import type { SmartContextConfig } from '../config/schema.js';
5
+ import type { ProviderAdapter } from '../providers/types.js';
6
+ import { Router } from './router.js';
7
+ import { streamResponse } from './stream.js';
8
+ import { ContextOptimizer } from '../context/optimizer.js';
9
+ import { MetricsCollector, type RequestMetric } from '../metrics/collector.js';
10
+ import type { EmbeddingAdapter } from '../embedding/types.js';
11
+ import type { StorageAdapter } from '../storage/types.js';
12
+ import { estimateTokens } from '../context/chunker.js';
13
+ import { getTextContent } from '../context/canonical.js';
14
+ import { renderDashboard } from '../ui/dashboard.js';
15
+
16
+ export class ProxyServer {
17
+ private server: http.Server;
18
+ private router: Router;
19
+ private config: SmartContextConfig;
20
+ private requestCount = 0;
21
+ private optimizer: ContextOptimizer | null = null;
22
+ private metrics = new MetricsCollector();
23
+ private paused = false;
24
+
25
+ constructor(
26
+ config: SmartContextConfig,
27
+ embedding?: EmbeddingAdapter,
28
+ storage?: StorageAdapter,
29
+ ) {
30
+ this.config = config;
31
+ this.router = new Router(config);
32
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
33
+
34
+ if (embedding && storage) {
35
+ this.optimizer = new ContextOptimizer(embedding, storage, config.context);
36
+ }
37
+ }
38
+
39
+ async start(): Promise<void> {
40
+ const { port, host } = this.config.proxy;
41
+ return new Promise((resolve) => {
42
+ this.server.listen(port, host, () => resolve());
43
+ });
44
+ }
45
+
46
+ async stop(): Promise<void> {
47
+ return new Promise((resolve) => {
48
+ this.server.close(() => resolve());
49
+ });
50
+ }
51
+
52
+ getProviderNames(): string[] {
53
+ return this.router.getProviderNames();
54
+ }
55
+
56
+ getMetrics(): MetricsCollector {
57
+ return this.metrics;
58
+ }
59
+
60
+ setPaused(paused: boolean): void {
61
+ this.paused = paused;
62
+ }
63
+
64
+ isPaused(): boolean {
65
+ return this.paused;
66
+ }
67
+
68
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
69
+ const path = req.url || '/';
70
+ const method = req.method || 'GET';
71
+
72
+ // Dashboard (root path)
73
+ if (path === '/' && method === 'GET') {
74
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
75
+ res.end(renderDashboard(this.metrics, this.paused));
76
+ return;
77
+ }
78
+
79
+ // Health check
80
+ if (path === '/health') {
81
+ res.writeHead(200, { 'Content-Type': 'application/json' });
82
+ res.end(JSON.stringify({
83
+ ok: true,
84
+ requests: this.requestCount,
85
+ paused: this.paused,
86
+ mode: this.optimizer ? 'optimizing' : 'transparent',
87
+ }));
88
+ return;
89
+ }
90
+
91
+ // Internal API endpoints (/_sc/*)
92
+ if (path.startsWith('/_sc/')) {
93
+ await this.handleApiRequest(path, method, req, res);
94
+ return;
95
+ }
96
+
97
+ // Only handle POST to /v1/{provider}/*
98
+ if (method !== 'POST') {
99
+ res.writeHead(405, { 'Content-Type': 'application/json' });
100
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
101
+ return;
102
+ }
103
+
104
+ const route = this.router.resolve(path);
105
+ if (!route) {
106
+ res.writeHead(404, { 'Content-Type': 'application/json' });
107
+ res.end(JSON.stringify({ error: `Unknown provider path: ${path}` }));
108
+ return;
109
+ }
110
+
111
+ try {
112
+ this.requestCount++;
113
+ await this.proxyRequest(req, res, route.adapter, path);
114
+ } catch (err) {
115
+ const message = err instanceof Error ? err.message : 'Internal proxy error';
116
+ this.log('error', `Proxy error: ${message}`);
117
+ if (!res.headersSent) {
118
+ res.writeHead(502, { 'Content-Type': 'application/json' });
119
+ res.end(JSON.stringify({ error: message }));
120
+ }
121
+ }
122
+ }
123
+
124
+ private async handleApiRequest(
125
+ path: string,
126
+ method: string,
127
+ req: IncomingMessage,
128
+ res: ServerResponse,
129
+ ): Promise<void> {
130
+ res.setHeader('Content-Type', 'application/json');
131
+
132
+ switch (path) {
133
+ case '/_sc/status':
134
+ res.end(JSON.stringify({
135
+ state: this.paused ? 'paused' : 'running',
136
+ uptime: this.metrics.getUptime(),
137
+ requests: this.requestCount,
138
+ mode: this.optimizer ? 'optimizing' : 'transparent',
139
+ }));
140
+ break;
141
+
142
+ case '/_sc/stats':
143
+ res.end(JSON.stringify(this.metrics.getStats()));
144
+ break;
145
+
146
+ case '/_sc/feed':
147
+ res.end(JSON.stringify(this.metrics.getRecent(50)));
148
+ break;
149
+
150
+ case '/_sc/pause':
151
+ this.paused = true;
152
+ res.end(JSON.stringify({ ok: true, state: 'paused' }));
153
+ break;
154
+
155
+ case '/_sc/resume':
156
+ this.paused = false;
157
+ res.end(JSON.stringify({ ok: true, state: 'running' }));
158
+ break;
159
+
160
+ default:
161
+ res.writeHead(404);
162
+ res.end(JSON.stringify({ error: `Unknown API path: ${path}` }));
163
+ }
164
+ }
165
+
166
+ private async proxyRequest(
167
+ clientReq: IncomingMessage,
168
+ clientRes: ServerResponse,
169
+ adapter: ProviderAdapter,
170
+ path: string,
171
+ ): Promise<void> {
172
+ const startTime = Date.now();
173
+
174
+ const bodyBuf = await this.readBody(clientReq);
175
+ const body = JSON.parse(bodyBuf.toString());
176
+
177
+ const headers: Record<string, string> = {};
178
+ for (const [key, val] of Object.entries(clientReq.headers)) {
179
+ if (typeof val === 'string') headers[key] = val;
180
+ }
181
+
182
+ const canonical = adapter.parseRequest(body, headers);
183
+ const originalTokens =
184
+ estimateTokens(canonical.systemPrompt || '') +
185
+ canonical.messages.reduce((sum, m) => sum + estimateTokens(getTextContent(m)), 0);
186
+
187
+ let forwardBody: string;
188
+ let optimizedTokens = originalTokens;
189
+ let savingsPercent = 0;
190
+ let chunksRetrieved = 0;
191
+ let topScore = 0;
192
+ let passThrough = true;
193
+ let reason: string | undefined;
194
+
195
+ // Context optimization (if available and not paused)
196
+ if (this.optimizer && !this.paused) {
197
+ try {
198
+ const result = await this.optimizer.optimize(canonical);
199
+ passThrough = result.passThrough;
200
+ reason = result.reason;
201
+
202
+ if (!result.passThrough) {
203
+ // Use optimized context
204
+ canonical.messages = result.optimizedMessages;
205
+ if (result.systemPrompt !== undefined) {
206
+ canonical.systemPrompt = result.systemPrompt;
207
+ }
208
+ optimizedTokens = result.packed.optimizedTokens;
209
+ savingsPercent = result.packed.savingsPercent;
210
+ }
211
+
212
+ if (result.retrieval) {
213
+ chunksRetrieved = result.retrieval.chunks.length;
214
+ topScore = result.retrieval.topScore;
215
+ }
216
+ } catch (err) {
217
+ // Graceful degradation: optimization failed, forward original
218
+ this.log('error', `Optimization failed, passing through: ${err}`);
219
+ passThrough = true;
220
+ reason = `optimization error: ${err}`;
221
+ }
222
+ }
223
+
224
+ // Serialize for forwarding
225
+ if (!passThrough) {
226
+ forwardBody = JSON.stringify(adapter.serializeRequest(canonical));
227
+ } else {
228
+ forwardBody = JSON.stringify(body);
229
+ }
230
+
231
+ const forwardUrl = new URL(adapter.forwardUrl(path));
232
+ const apiKey = canonical.providerAuth || this.config.providers[adapter.name]?.apiKey || '';
233
+
234
+ const forwardHeaders: Record<string, string> = {
235
+ 'Content-Type': adapter.contentType(),
236
+ ...adapter.authHeaders(apiKey),
237
+ };
238
+
239
+ if (headers['anthropic-version']) forwardHeaders['anthropic-version'] = headers['anthropic-version'];
240
+ if (headers['anthropic-beta']) forwardHeaders['anthropic-beta'] = headers['anthropic-beta'];
241
+ forwardHeaders['Content-Length'] = Buffer.byteLength(forwardBody).toString();
242
+
243
+ const latencyOverhead = Date.now() - startTime;
244
+ const savingsStr = passThrough ? 'pass' : `-${savingsPercent}%`;
245
+
246
+ this.log('info',
247
+ `#${this.requestCount} ${adapter.name}/${canonical.model} ` +
248
+ `${originalTokens}→${optimizedTokens} ${savingsStr} ` +
249
+ `${canonical.stream ? 'stream' : 'sync'} ${latencyOverhead}ms`);
250
+
251
+ // Forward to provider
252
+ const transport = forwardUrl.protocol === 'https:' ? https : http;
253
+ const providerRes = await new Promise<IncomingMessage>((resolve, reject) => {
254
+ const proxyReq = transport.request(forwardUrl, { method: 'POST', headers: forwardHeaders }, resolve);
255
+ proxyReq.on('error', reject);
256
+ proxyReq.write(forwardBody);
257
+ proxyReq.end();
258
+ });
259
+
260
+ // Add debug headers if enabled
261
+ if (this.config.logging.debug_headers && !passThrough) {
262
+ providerRes.headers['x-smartcontext-savings'] = `${savingsPercent}%`;
263
+ providerRes.headers['x-smartcontext-original-tokens'] = String(originalTokens);
264
+ providerRes.headers['x-smartcontext-optimized-tokens'] = String(optimizedTokens);
265
+ providerRes.headers['x-smartcontext-chunks'] = String(chunksRetrieved);
266
+ providerRes.headers['x-smartcontext-latency-ms'] = String(latencyOverhead);
267
+ providerRes.headers['x-smartcontext-mode'] = this.paused ? 'paused' : 'optimized';
268
+ }
269
+
270
+ // Stream response back
271
+ const responseBuffer = await streamResponse(providerRes, clientRes);
272
+
273
+ // Record metrics
274
+ this.metrics.record({
275
+ id: this.requestCount,
276
+ timestamp: Date.now(),
277
+ provider: adapter.name,
278
+ model: canonical.model,
279
+ streaming: canonical.stream,
280
+ originalTokens,
281
+ optimizedTokens,
282
+ savingsPercent,
283
+ latencyOverheadMs: latencyOverhead,
284
+ chunksRetrieved,
285
+ topScore,
286
+ passThrough,
287
+ reason,
288
+ });
289
+
290
+ // Async post-indexing (don't block response)
291
+ if (this.optimizer && !passThrough) {
292
+ const sessionId = canonical.rawHeaders['x-smartcontext-session'] || `auto-${this.requestCount}`;
293
+ this.optimizer.indexExchange(canonical.messages, sessionId).catch((err) => {
294
+ this.log('error', `Post-indexing failed: ${err}`);
295
+ });
296
+ }
297
+ }
298
+
299
+ private readBody(req: IncomingMessage): Promise<Buffer> {
300
+ return new Promise((resolve, reject) => {
301
+ const chunks: Buffer[] = [];
302
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
303
+ req.on('end', () => resolve(Buffer.concat(chunks)));
304
+ req.on('error', reject);
305
+ });
306
+ }
307
+
308
+ private log(level: string, message: string): void {
309
+ const timestamp = new Date().toISOString().slice(11, 23);
310
+ const prefix = level === 'error' ? '✗' : '→';
311
+ if (level === 'error' || this.config.logging.level !== 'error') {
312
+ console.log(`[${timestamp}] ${prefix} ${message}`);
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,39 @@
1
+ import { IncomingMessage } from 'node:http';
2
+ import type { ServerResponse } from 'node:http';
3
+
4
+ /**
5
+ * Stream SSE response from provider to client byte-by-byte.
6
+ * Zero buffering — passes through as fast as possible.
7
+ * Returns the full buffered response body for post-indexing.
8
+ */
9
+ export async function streamResponse(
10
+ providerRes: IncomingMessage,
11
+ clientRes: ServerResponse,
12
+ ): Promise<Buffer> {
13
+ return new Promise((resolve, reject) => {
14
+ const chunks: Buffer[] = [];
15
+
16
+ // Copy status and headers
17
+ clientRes.writeHead(providerRes.statusCode || 200, providerRes.headers);
18
+
19
+ providerRes.on('data', (chunk: Buffer) => {
20
+ chunks.push(chunk);
21
+ clientRes.write(chunk);
22
+ });
23
+
24
+ providerRes.on('end', () => {
25
+ clientRes.end();
26
+ resolve(Buffer.concat(chunks));
27
+ });
28
+
29
+ providerRes.on('error', (err) => {
30
+ clientRes.end();
31
+ reject(err);
32
+ });
33
+
34
+ // Handle client disconnect
35
+ clientRes.on('close', () => {
36
+ providerRes.destroy();
37
+ });
38
+ });
39
+ }