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